Compare commits

...

150 Commits

Author SHA1 Message Date
e627a8b963 Fix for older HA versions 2020-04-04 13:17:01 +00:00
a7c3b46061 Update pubspec.yaml 2020-04-03 17:25:59 +03:00
dfbaaeb06b 0.8.0 2020-04-03 13:32:23 +00:00
f6ab20c6e8 Add whats new manu item 2020-04-03 13:29:02 +00:00
7625099d74 Disable background location log 2020-04-03 13:17:24 +00:00
32c8e76855 Fix for non-string climate mode values 2020-04-03 13:07:26 +00:00
0aa2c974d5 Disable crashlytics in debug mode 2020-04-03 12:43:48 +00:00
9524c8587b Update WHats new link 2020-04-03 12:36:16 +00:00
c075db8b1a 883 2020-04-02 22:05:36 +00:00
d0b7cc1929 Initial UI generation if no lovelace config 2020-04-02 22:04:56 +00:00
d8df32f140 Resolves #524 Geolocator update 2020-04-02 21:16:34 +00:00
293b5e0242 zha_map WIP: get inital data 2020-04-01 17:04:32 +00:00
2f517a3ad5 ui build refactoring 1 2020-03-21 23:11:00 +00:00
56d8e389db Do not update plugins on each gitpod start 2020-03-21 20:45:01 +00:00
1377843350 882 2020-03-21 14:06:58 +00:00
8e31eaf8bb Fix dashboard switching and dashboard icons 2020-03-20 10:14:14 +00:00
5ced01463f Resolves #526 Subscribe to Lovelace update events 2020-03-19 23:16:59 +00:00
a3548455eb Resolves #525 Support Lovelace dashboards 2020-03-19 22:59:53 +00:00
c40fceea4f add log output on location error in background 2020-03-15 17:51:01 +00:00
6ad3938a91 Resolves #369 Get actual entity data in entity wrapper 2020-03-15 16:28:45 +00:00
bc642f81ad Fix caching 2020-03-15 15:26:03 +00:00
14ce608bbb Add data caching 2020-03-15 13:47:51 +00:00
c4c67747c5 Remove services requesting 2020-03-15 12:34:16 +00:00
5b3ceecb0e Update crashlytics 2020-03-15 12:27:32 +00:00
bf53e4b9df Add fit option for EntityPicture widget 2020-03-14 18:23:39 +00:00
7e09d92fdf Add double_tap_action support 2020-03-14 18:12:11 +00:00
1ba9106d0b Show entity picture while camera stream loading 2020-03-14 17:56:07 +00:00
d727a29991 881 2020-03-12 21:20:46 +00:00
c5d617477f Fix state filter when state is number 2020-03-12 21:19:39 +00:00
244a1984cc Resolves #512 Support all state_filter options 2020-03-12 21:16:07 +00:00
b00b745f27 Camera fullscreen view 2020-03-12 12:22:38 +00:00
959ff21b9b Fix checking log file size if it doesnt exist 2020-03-09 15:01:53 +00:00
e6a7fd2dfe workmanager 0.2.2, geolocator 5.3.0, crashlytics 0.1.3 2020-03-09 14:57:29 +00:00
216276e5f3 Limit log file size to 5MB. Background location fixes 2020-03-09 14:53:12 +00:00
3e6229cf3e Add log file for background service 2020-03-09 13:11:16 +00:00
fc4cb80b74 Fix background task execution 2020-03-05 09:08:47 +00:00
b907ff1e82 Location isolate return 2020-03-04 17:16:16 +00:00
7536a52771 Clear camera viewer 2020-03-04 16:59:08 +00:00
73a8c111d1 Camera stream controls 2020-02-21 15:36:03 +00:00
86a19eeec2 Camera stream web view improvements 2020-02-21 11:23:39 +00:00
fba4459977 Camera stream view fixes 2020-02-20 22:42:19 +00:00
06f994a827 Camera stream aspect ratio calculations 2020-02-20 21:48:22 +00:00
35d8607484 Resolves #370 Camera stream support 2020-02-20 14:33:03 +00:00
2f4c06e9b5 Resolves #420 Camera view on entity page 2020-02-20 11:18:09 +00:00
92e008a380 Resolves #522 Fix temperature change for thermostat 2020-02-19 14:20:44 +00:00
14c272af92 Resolves #501 Entities card header toggle 2020-02-19 10:17:08 +00:00
710de9f2b8 Resolves #514 Media player volume buttons placement 2020-02-19 09:27:01 +00:00
d9ad3b3083 Location tracking... 2020-02-17 19:23:00 +00:00
b2686cb105 Update README.md 2020-02-17 16:39:30 +02:00
959e89de2b Remove web UI presentation option 2020-02-17 13:50:24 +00:00
6e448d3458 JS interface improvements 2020-02-12 22:42:43 +00:00
6695756727 Embedded webview 2020-02-12 21:13:49 +00:00
ed732e9b77 Remove bottom bar for webview 2020-02-12 13:37:52 +00:00
f495a6affc Move to ha-client.app 2020-02-12 13:22:48 +00:00
c8d7e1a95f Update README.md 2020-02-12 12:39:36 +02:00
e1ca2638e3 External bus and configuration opening for Web UI 2020-02-11 22:55:16 +00:00
01226cb9eb WebVIew UI settings 2020-02-11 20:53:29 +00:00
8a80d0c5d1 WebView UI experiment 001 2020-02-11 14:02:32 +00:00
f26f3e87c7 Remove Crashlytics debug 2020-02-11 12:57:31 +00:00
b750417415 Replace Sentry with Crashlytics 2020-02-11 12:06:19 +00:00
2c35dd7c21 Update battery plugin 2020-02-11 11:25:58 +00:00
cff4a4feed Update workmanager and migrate to Flutter embedding v2 2020-02-11 11:22:59 +00:00
62174b0651 Update geolocator plugin 2020-02-11 11:07:13 +00:00
d3ea4210c1 Update local notifications library 2020-02-11 11:03:39 +00:00
1c782bf64d Update firebase library and downgrade gradle 2020-02-11 10:57:15 +00:00
bc96dab339 Update gradle and Google services 2020-02-11 09:52:04 +00:00
0f7179b944 In app purchase update and optimizations 2020-02-11 09:39:11 +00:00
1e3bfa8ff7 Install Java in gitpod with sdkman 2020-02-07 09:46:34 +00:00
2bce86f905 gitpod config 2020-02-07 09:27:23 +00:00
0be00acc3a do 2020-01-30 20:50:42 +00:00
4e61adaeb1 Fixind java version switch 2020-01-30 20:42:29 +00:00
49a8f08153 Update .gitpod.dockerfile 2020-01-30 22:34:10 +02:00
ce15658462 Add jdk version set 2020-01-30 20:28:34 +00:00
16d73ba7dd Update .gitpod.yml 2020-01-30 22:20:54 +02:00
9f3e3c1917 Update .gitpod.dockerfile 2020-01-30 22:20:01 +02:00
f29e382a19 Update .gitpod.yml 2020-01-30 21:53:29 +02:00
073562373a Update .gitpod.dockerfile 2020-01-30 21:43:28 +02:00
4298ebcd66 Bump Flutter version to 1.12.13 2020-01-29 20:13:28 +02:00
a121295bef Update .gitignore 2020-01-29 20:12:46 +02:00
9303e4c0a5 Force https for oauth 2020-01-29 17:38:51 +00:00
831fc98ab1 Change domain for ha-client website 2020-01-29 17:31:16 +00:00
2003005e56 Update README.md 2020-01-29 11:48:32 +02:00
fda8fb7182 Webview for external panels and everything 2020-01-27 21:25:55 +00:00
cf6039b279 Back to webview oauth 2020-01-27 21:12:05 +00:00
41e552dce5 New webview test 2020-01-27 20:06:02 +00:00
90043b5806 Remove text sharing feature 2020-01-27 19:21:21 +00:00
9eb74b5a8d WIP Share media refactoring 2019-12-11 18:24:09 +00:00
9cc60a136b 0.7.7 2019-12-10 22:05:12 +00:00
78eb1e779c Fix state event handling before fetch complete 2019-12-10 22:04:24 +00:00
8db2d8508e Resolves #515 tabs controller issue 2019-12-10 22:00:17 +00:00
3f1ece26ec Remove background task logging and reporting 2019-12-10 21:19:29 +00:00
d1912a44c6 Replace Discord with Spectrum 2019-12-10 19:54:02 +00:00
36a05eb390 Update README.md 2019-12-06 10:58:23 +02:00
4f39ea1ad8 Update README.md 2019-12-04 16:31:03 +02:00
a241cc1d61 Resolves #494 2019-11-29 13:40:51 +00:00
8b4df98cb9 0.7.6 2019-11-29 13:02:22 +00:00
7d30c2f9d5 Wrap empty navigate action 2019-11-29 12:45:59 +00:00
44acabadfe Fix location tracking backfroud task id 2019-11-29 11:27:59 +00:00
6f3a2bb78d Fix timeout handling on socket message send 2019-11-29 11:24:29 +00:00
dd5f8b155d Handle some socket exceptions 2019-11-29 11:21:45 +00:00
cd81fc72fd Fix connection timeout handling 2019-11-29 10:58:24 +00:00
890da650dc Resolves #508 show_name for enriry button card 2019-11-29 10:12:41 +00:00
9897b6a44b Fix show_empty for entity-filter 2019-11-29 10:05:09 +00:00
7969f54d3b Fix MissedPluginException for workmanager 2019-11-28 19:54:22 +00:00
7c18454de3 Fix issue with handling service call exceptions 2019-11-28 19:14:50 +00:00
dcf5efddd1 Parse port and protocol from HA url 2019-11-28 18:57:41 +00:00
a6541134e0 Fix compliting alrady completed future 2019-11-28 18:33:27 +00:00
90504047b4 Resolves #492 Infinity media player progress error 2019-11-28 17:42:48 +00:00
ca1eec6602 Update bug_report.md 2019-11-27 18:44:45 +02:00
edc01d14b7 Resolves #511 2019-11-27 17:21:23 +02:00
6cb5463b13 minor background reporting fix 2019-11-27 14:50:53 +00:00
63a789ebfb 0.7.5 name fix 2019-11-27 12:48:03 +00:00
a0994e9a60 0.7.5 2019-11-27 12:42:09 +00:00
8d1b728194 Background location tracking crash reporting 2019-11-27 12:41:38 +00:00
1a9fec8b98 Senty reporting. Fix background location tracking crash 2019-11-27 12:26:55 +00:00
e634253282 0.7.4 2019-11-26 21:13:59 +00:00
64b23ec7cc Revert flutter_markdown to 0.3.0 2019-11-26 21:13:07 +00:00
afe207a878 Removes foreground location and Resolves #510 2019-11-26 20:56:24 +00:00
4bac0c092f Removes foreground location and Resolves #510 2019-11-26 20:54:36 +00:00
74c8ae35a1 Remove network security config 2019-11-26 20:48:08 +00:00
7856637456 Fix app version display 2019-11-26 16:50:19 +00:00
965f80a6ca 0.7.3 2019-11-14 12:58:56 +02:00
198c2ba49a build 730 2019-11-14 12:58:32 +02:00
4b9ec5ca6e Foreground location updates 2019-11-10 21:53:28 +00:00
5792652619 Experimental location tracking for every 10 or 5 minutes 2019-11-10 14:41:29 +00:00
2c900333a5 WIP #344 Add network security config to allow user certificates 2019-11-10 13:53:25 +00:00
1f782d7cd3 Resolves #498 Handle bool state 2019-11-10 13:48:05 +00:00
89cc1833de Resolves #493 Send media_player calls even if it if unavailable 2019-11-08 20:50:31 +00:00
1262d8c9aa Resolves #484 Fix entity-filter cards 2019-11-08 20:41:51 +00:00
85b0c4f814 Resolves #419 Fallback to states if no lovelace config found 2019-11-08 20:14:34 +00:00
551a8dfa31 Fixx service calls 2019-11-08 19:37:41 +00:00
139533d2ca 0.7.2 2019-11-01 14:00:13 +00:00
889682f771 Resolves #491 Lovelace badges parse issue 2019-11-01 13:54:35 +00:00
f16c98057f Location fixes 2019-11-01 13:44:51 +00:00
26ec807c25 Resolves #490 Prevent fused coarse location 2019-10-30 16:54:25 +00:00
45af6cbe3c Fix play_media call 2019-10-30 15:04:23 +00:00
5dd9cde12d Entity page fixes 2019-10-30 14:25:30 +00:00
472fb1d367 Merge pull request #489 from estevez-dev/hotfix/0.7.1
Hotfix/0.7.1
2019-10-29 19:47:33 +02:00
8b372fbc0b Merge branch 'master' into hotfix/0.7.1 2019-10-29 19:47:24 +02:00
d1f652282a Merge pull request #488 from estevez-dev/pre-release/0.7.1
Pre release/0.7.1
2019-10-29 19:35:10 +02:00
f656528d5b 711 2019-10-28 20:42:05 +00:00
bcdb2a648c Fix entity page linking after app resumed 2019-10-28 20:40:30 +00:00
8a78745aa7 State change event log 2019-10-28 20:34:43 +00:00
2a3eaabbe4 Remove some logs 2019-10-28 20:32:23 +00:00
bcd175fbfb Fix service calls 2019-10-28 20:31:45 +00:00
f9f013636d 710 2019-10-28 18:02:03 +00:00
b34cc97080 Show entity page on main page 2019-10-28 17:59:47 +00:00
327f623ef7 Remove HA url from main manu 2019-10-28 16:39:47 +00:00
4d0877e5ae Remove Profile link 2019-10-28 16:36:20 +00:00
0eac217399 Set minimum location update intervhals to 15 minute 2019-10-28 16:28:01 +00:00
75 changed files with 2030 additions and 1080 deletions

View File

@ -7,35 +7,16 @@ assignees: ''
--- ---
<!-- **HA Client version:** [Main menu -> About HA Client]
Please provide as much information as possible.
-->
**HA Client version:** <!-- Main app menu => About HA Client -->
**Home Assistant version:** <!-- 0.94.1 for example --> **Home Assistant version:**
**Device name:** <!-- Pixel 2 for example --> **Device name:**
**Android version:** <!-- 8.1 for example --> **Android version:**
**Connection type:** <!-- For example "Local IP" or "Remote UI" or "Own domain"-->
**Login type:** <!-- For example "HA Login" or "Manual token"-->
**Description** **Description**
<!-- [Replace with description]
Describe your issue here
-->
**Screenshots** **Screenshots**
<!-- [Replace with screenshots]
Please provide screenshots if it is a UI issue. Also you can attach screenshot from Home Assistant web UI as an expected result
-->
**Logs**
<!--
Right after issue reproduced go to app menu and tap "Log". Copy log with a "Copy" button in the upper-right corner and post it below
-->
```
[Replace this text with your logs]
```

3
.gitignore vendored
View File

@ -15,7 +15,8 @@ build/
.settings/ .settings/
flutter_export_environment.sh flutter_export_environment.sh
.flutter-plugins-dependencies
key.properties key.properties
premium_features_manager.class.dart premium_features_manager.class.dart
pubspec.lock pubspec.lock

View File

@ -4,9 +4,5 @@ ENV ANDROID_HOME=/workspace/android-sdk \
FLUTTER_ROOT=/workspace/flutter \ FLUTTER_ROOT=/workspace/flutter \
FLUTTER_HOME=/workspace/flutter FLUTTER_HOME=/workspace/flutter
USER root RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \
&& sdk install java 8.0.242.j9-adpt"
RUN apt-get update && \
apt-get -y install build-essential libkrb5-dev gcc make gradle openjdk-8-jdk && \
apt-get clean && \
apt-get -y autoremove

View File

@ -8,7 +8,7 @@ tasks:
touch /home/gitpod/.android/repositories.cfg touch /home/gitpod/.android/repositories.cfg
init: | init: |
echo "Installing Flutter SDK..." echo "Installing Flutter SDK..."
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.9.1+hotfix.4-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz 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..." 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 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" /workspace/android-sdk/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3"
@ -18,7 +18,6 @@ tasks:
flutter doctor --android-licenses flutter doctor --android-licenses
flutter pub get flutter pub get
command: | command: |
flutter pub upgrade
echo "Ready to go!" echo "Ready to go!"
flutter doctor flutter doctor
vscode: vscode:

Binary file not shown.

Binary file not shown.

View File

View File

@ -1,13 +1,14 @@
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
# HA Client # HA Client
## Native Android client for Home Assistant ## Native Android client for Home Assistant
### With notifications and 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) 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)
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
#### Pre-release CI build #### Pre-release CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build) [![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)

View File

@ -78,10 +78,11 @@ flutter {
} }
dependencies { dependencies {
implementation 'com.google.firebase:firebase-core:16.0.8' implementation 'com.google.firebase:firebase-analytics:17.2.2'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
} }
apply plugin: 'io.fabric'
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'

View File

@ -3,17 +3,13 @@
<uses-feature android:name="android.hardware.touchscreen" <uses-feature android:name="android.hardware.touchscreen"
android:required="false" /> android:required="false" />
<!-- 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-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.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.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- io.flutter.app.FlutterApplication is an android.app.Application that <!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method. calls FlutterMain.startInitialization(this); in its onCreate method.
@ -21,11 +17,14 @@
additional functionality it is fine to subclass or reimplement additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. --> FlutterApplication and put your custom class here. -->
<application <application
android:name=".Application"
android:label="HA Client" android:label="HA Client"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data <meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id" android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="ha_notify" /> android:value="ha_notify" />
@ -37,13 +36,12 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme).
<meta-data <meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame" android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:value="true" />--> android:resource="@drawable/launch_background" />
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter> <intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" /> <action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -52,14 +50,6 @@
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="haclient"
android:host="auth" />
</intent-filter>
</activity> </activity>
<service <service

View File

@ -1,20 +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;
import be.tramckrijte.workmanager.WorkmanagerPlugin;
public class Application extends FlutterApplication implements PluginRegistrantCallback {
@Override
public void onCreate() {
super.onCreate();
WorkmanagerPlugin.setPluginRegistrantCallback(this);
}
@Override
public void registerWith(PluginRegistry registry) {
GeneratedPluginRegistrant.registerWith(registry);
}
}

View File

@ -1,15 +1,15 @@
package com.keyboardcrumbs.hassclient; package com.keyboardcrumbs.hassclient;
import android.os.Bundle; import androidx.annotation.NonNull;
import io.flutter.app.FlutterActivity; import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant; import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugins.share.FlutterShareReceiverActivity;
public class MainActivity extends FlutterShareReceiverActivity { public class MainActivity extends FlutterActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(flutterEngine);
GeneratedPluginRegistrant.registerWith(this); }
}
} }

View File

@ -5,4 +5,7 @@
Flutter draws its first frame --> Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item> <item name="android:windowBackground">@drawable/launch_background</item>
</style> </style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
</resources> </resources>

View File

@ -2,11 +2,15 @@ buildscript {
repositories { repositories {
google() google()
jcenter() jcenter()
maven {
url 'https://maven.fabric.io/public'
}
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.3.2' classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.google.gms:google-services:4.2.0' classpath 'com.google.gms:google-services:4.3.3'
classpath 'io.fabric.tools:gradle:1.26.1'
} }
} }
@ -14,6 +18,9 @@ allprojects {
repositories { repositories {
google() google()
jcenter() jcenter()
maven {
url 'https://maven.fabric.io/public'
}
} }
} }

View File

@ -2,4 +2,5 @@ org.gradle.jvmargs=-Xmx2g
org.gradle.daemon=true org.gradle.daemon=true
org.gradle.caching=true org.gradle.caching=true
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
android.enableR8=true

View File

@ -0,0 +1 @@
include ':app'

View 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>

View File

@ -13,4 +13,23 @@ window.externalApp.getExternalAuth = function(options) {
window[options.callback](true, responseData); window[options.callback](true, responseData);
}, 500); }, 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');
}
}; };

View File

@ -10,6 +10,7 @@ class HACard {
bool showName; bool showName;
bool showState; bool showState;
bool showEmpty; bool showEmpty;
bool showHeaderToggle;
int columnsCount; int columnsCount;
List stateFilter; List stateFilter;
List states; List states;
@ -26,6 +27,7 @@ class HACard {
this.linkedEntityWrapper, this.linkedEntityWrapper,
this.columnsCount: 4, this.columnsCount: 4,
this.showName: true, this.showName: true,
this.showHeaderToggle: true,
this.showState: true, this.showState: true,
this.stateFilter: const [], this.stateFilter: const [],
this.showEmpty: true, this.showEmpty: true,
@ -45,13 +47,70 @@ class HACard {
List<EntityWrapper> getEntitiesToShow() { List<EntityWrapper> getEntitiesToShow() {
return entities.where((entityWrapper) { return entities.where((entityWrapper) {
if (!ConnectionManager().useLovelace && entityWrapper.entity.isHidden) { if (HomeAssistant().autoUi && entityWrapper.entity.isHidden) {
return false; return false;
} }
if (stateFilter.isNotEmpty) { List currentStateFilter;
return stateFilter.contains(entityWrapper.entity.state); if (entityWrapper.stateFilter != null && entityWrapper.stateFilter.isNotEmpty) {
currentStateFilter = entityWrapper.stateFilter;
} else {
currentStateFilter = stateFilter;
} }
return true; 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(); }).toList();
} }

View File

@ -132,7 +132,33 @@ class CardWidget extends StatelessWidget {
return Container(height: 0.0, width: 0.0,); return Container(height: 0.0, width: 0.0,);
} }
List<Widget> body = []; List<Widget> body = [];
body.add(CardHeader(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) { entitiesToShow.forEach((EntityWrapper entity) {
body.add( body.add(
Padding( Padding(
@ -280,21 +306,23 @@ class CardWidget extends StatelessWidget {
} }
Widget _buildEntityButtonCard(BuildContext context) { Widget _buildEntityButtonCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ?? card.linkedEntityWrapper.overrideName = card.name?.toUpperCase() ??
card.linkedEntityWrapper.displayName.toUpperCase(); card.linkedEntityWrapper.displayName.toUpperCase();
return Card( return Card(
child: EntityModel( child: EntityModel(
entityWrapper: card.linkedEntityWrapper, entityWrapper: card.linkedEntityWrapper,
child: EntityButtonCardBody(), child: EntityButtonCardBody(
showName: card.showName,
),
handleTap: true handleTap: true
) )
); );
} }
Widget _buildGaugeCard(BuildContext context) { Widget _buildGaugeCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name ?? card.linkedEntityWrapper.overrideName = card.name ??
card.linkedEntityWrapper.displayName; card.linkedEntityWrapper.displayName;
card.linkedEntityWrapper.unitOfMeasurement = card.unit ?? card.linkedEntityWrapper.unitOfMeasurementOverride = card.unit ??
card.linkedEntityWrapper.unitOfMeasurement; card.linkedEntityWrapper.unitOfMeasurement;
return Card( return Card(
child: EntityModel( child: EntityModel(
@ -310,7 +338,7 @@ class CardWidget extends StatelessWidget {
} }
Widget _buildLightCard(BuildContext context) { Widget _buildLightCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name ?? card.linkedEntityWrapper.overrideName = card.name ??
card.linkedEntityWrapper.displayName; card.linkedEntityWrapper.displayName;
return Card( return Card(
child: EntityModel( child: EntityModel(
@ -327,7 +355,11 @@ class CardWidget extends StatelessWidget {
Widget _buildUnsupportedCard(BuildContext context) { Widget _buildUnsupportedCard(BuildContext context) {
List<Widget> body = []; List<Widget> body = [];
body.add(CardHeader(name: card.name ?? "")); body.add(
CardHeader(
name: card.name ?? ""
)
);
List<Widget> result = []; List<Widget> result = [];
if (card.linkedEntityWrapper != null) { if (card.linkedEntityWrapper != null) {
result.addAll(<Widget>[ result.addAll(<Widget>[

View File

@ -2,8 +2,10 @@ part of '../../main.dart';
class EntityButtonCardBody extends StatelessWidget { class EntityButtonCardBody extends StatelessWidget {
final bool showName;
EntityButtonCardBody({ EntityButtonCardBody({
Key key, Key key, this.showName: true,
}) : super(key: key); }) : super(key: key);
@override @override
@ -19,6 +21,7 @@ class EntityButtonCardBody extends StatelessWidget {
return InkWell( return InkWell(
onTap: () => entityWrapper.handleTap(), onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(), onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: FractionallySizedBox( child: FractionallySizedBox(
widthFactor: 1, widthFactor: 1,
child: Column( child: Column(
@ -39,13 +42,16 @@ class EntityButtonCardBody extends StatelessWidget {
} }
Widget _buildName() { Widget _buildName() {
return EntityName( if (showName) {
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding), return EntityName(
textOverflow: TextOverflow.ellipsis, padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
maxLines: 3, textOverflow: TextOverflow.ellipsis,
wordsWrap: true, maxLines: 3,
textAlign: TextAlign.center, wordsWrap: true,
fontSize: Sizes.nameFontSize, textAlign: TextAlign.center,
); fontSize: Sizes.nameFontSize,
);
}
return Container(width: 0, height: 0);
} }
} }

View File

@ -64,6 +64,7 @@ class _GaugeCardBodyState extends State<GaugeCardBody> {
return InkWell( return InkWell(
onTap: () => entityWrapper.handleTap(), onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(), onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: AspectRatio( child: AspectRatio(
aspectRatio: 1.5, aspectRatio: 1.5,
child: Stack( child: Stack(

View File

@ -60,6 +60,7 @@ class GlanceCardEntityContainer extends StatelessWidget {
), ),
onTap: () => entityWrapper.handleTap(), onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(), onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
), ),
); );
} }

View File

@ -37,6 +37,7 @@ class _LightCardBodyState extends State<LightCardBody> {
return InkWell( return InkWell(
onTap: () => entityWrapper.handleTap(), onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(), onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: AspectRatio( child: AspectRatio(
aspectRatio: 1.5, aspectRatio: 1.5,
child: Stack( child: Stack(

View File

@ -51,6 +51,10 @@ class EntityUIAction {
String holdNavigationPath; String holdNavigationPath;
String holdService; String holdService;
Map<String, dynamic> holdServiceData; Map<String, dynamic> holdServiceData;
String doubleTapAction = EntityUIAction.none;
String doubleTapNavigationPath;
String doubleTapService;
Map<String, dynamic> doubleTapServiceData;
EntityUIAction({rawEntityData}) { EntityUIAction({rawEntityData}) {
if (rawEntityData != null) { if (rawEntityData != null) {
@ -76,6 +80,17 @@ class EntityUIAction {
holdServiceData = rawEntityData["hold_action"]["service_data"]; holdServiceData = rawEntityData["hold_action"]["service_data"];
} }
} }
if (rawEntityData["double_tap_action"] != null) {
if (rawEntityData["double_tap_action"] is String) {
doubleTapAction = rawEntityData["double_tap_action"];
} else {
doubleTapAction =
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
doubleTapService = rawEntityData["double_tap_action"]["service"];
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
}
}
} }
} }

View File

@ -25,9 +25,12 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
void _callService(AlarmControlPanelEntity entity, String service) { void _callService(AlarmControlPanelEntity entity, String service) {
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, service, entity.entityId, domain: entity.domain,
{"code": "$code"})); service: service,
entityId: entity.entityId,
data: {"code": "$code"}
);
setState(() { setState(() {
code = ""; code = "";
}); });
@ -58,7 +61,11 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
FlatButton( FlatButton(
child: new Text("Yes"), child: new Text("Yes"),
onPressed: () { onPressed: () {
eventBus.fire(new ServiceCallEvent(entity.domain, "alarm_trigger", entity.entityId, null)); ConnectionManager().callService(
domain: entity.domain,
service: "alarm_trigger",
entityId: entity.entityId
);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),

View File

@ -3,12 +3,16 @@ part of '../../main.dart';
class CameraEntity extends Entity { class CameraEntity extends Entity {
static const SUPPORT_ON_OFF = 1; static const SUPPORT_ON_OFF = 1;
static const SUPPORT_STREAM = 2;
CameraEntity(Map rawData, String webHost) : super(rawData, webHost); CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportOnOff => ((supportedFeatures & bool get supportOnOff => ((supportedFeatures &
CameraEntity.SUPPORT_ON_OFF) == CameraEntity.SUPPORT_ON_OFF) ==
CameraEntity.SUPPORT_ON_OFF); CameraEntity.SUPPORT_ON_OFF);
bool get supportStream => ((supportedFeatures &
CameraEntity.SUPPORT_STREAM) ==
CameraEntity.SUPPORT_STREAM);
@override @override
Widget _buildAdditionalControlsForPage(BuildContext context) { Widget _buildAdditionalControlsForPage(BuildContext context) {

View File

@ -2,7 +2,9 @@ part of '../../../main.dart';
class CameraStreamView extends StatefulWidget { class CameraStreamView extends StatefulWidget {
CameraStreamView({Key key}) : super(key: key); final bool withControls;
CameraStreamView({Key key, this.withControls: true}) : super(key: key);
@override @override
_CameraStreamViewState createState() => _CameraStreamViewState(); _CameraStreamViewState createState() => _CameraStreamViewState();
@ -10,49 +12,235 @@ class CameraStreamView extends StatefulWidget {
class _CameraStreamViewState extends State<CameraStreamView> { 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 @override
void initState() { void initState() {
super.initState(); super.initState();
} }
CameraEntity _entity; Future _loadResources() {
bool started = false; if (_loading != null && !_loading.isCompleted) {
String streamUrl = ""; 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;
}
launchStream() { void createPlayer(data) {
Launcher.launchURLInCustomTab( _videoPlayerController = VideoPlayerController.network("${ConnectionManager().httpWebHost}${data["url"]}");
context: context, _videoPlayerController.initialize().then((_) {
url: streamUrl 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!started) { if (!_isLoaded && (_loading == null || _loading.isCompleted)) {
_entity = EntityModel _loadResources().then((_) => setState((){ _isLoaded = true; }));
.of(context)
.entityWrapper
.entity;
started = true;
} }
streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity if (widget.withControls) {
.entityId}?token=${_entity.attributes['access_token']}'; return Card(
return Column( child: Column(
children: <Widget>[ mainAxisSize: MainAxisSize.min,
Container( children: <Widget>[
padding: const EdgeInsets.all(20.0), _buildScreen(),
child: IconButton( _buildControls()
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber), ],
iconSize: 50.0, ),
onPressed: () => launchStream(), );
) } else {
) return _buildScreen();
], }
);
} }
@override @override
void dispose() { void dispose() {
_monitorTimer?.cancel();
_videoPlayerController?.dispose();
super.dispose(); super.dispose();
} }
} }

View File

@ -10,9 +10,8 @@ class ClimateControlWidget extends StatefulWidget {
class _ClimateControlWidgetState extends State<ClimateControlWidget> { class _ClimateControlWidgetState extends State<ClimateControlWidget> {
bool _showPending = false; bool _temperaturePending = false;
bool _changedHere = false; bool _changedHere = false;
Timer _resetTimer;
Timer _tempThrottleTimer; Timer _tempThrottleTimer;
Timer _targetTempThrottleTimer; Timer _targetTempThrottleTimer;
double _tmpTemperature = 0.0; double _tmpTemperature = 0.0;
@ -27,9 +26,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
bool _tmpAuxHeat = false; bool _tmpAuxHeat = false;
void _resetVars(ClimateEntity entity) { void _resetVars(ClimateEntity entity) {
_tmpTemperature = entity.temperature; if (!_temperaturePending) {
_tmpTargetHigh = entity.targetHigh; _tmpTemperature = entity.temperature;
_tmpTargetLow = entity.targetLow; _tmpTargetHigh = entity.targetHigh;
_tmpTargetLow = entity.targetLow;
}
_tmpHVACMode = entity.state; _tmpHVACMode = entity.state;
_tmpFanMode = entity.fanMode; _tmpFanMode = entity.fanMode;
_tmpSwingMode = entity.swingMode; _tmpSwingMode = entity.swingMode;
@ -38,7 +39,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
_tmpAuxHeat = entity.auxHeat; _tmpAuxHeat = entity.auxHeat;
_tmpTargetHumidity = entity.targetHumidity; _tmpTargetHumidity = entity.targetHumidity;
_showPending = false;
_changedHere = false; _changedHere = false;
} }
@ -73,36 +73,44 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
} }
void _setTemperature(ClimateEntity entity) { void _setTemperature(ClimateEntity entity) {
if (_tempThrottleTimer!=null) { _tempThrottleTimer?.cancel();
_tempThrottleTimer.cancel();
}
setState(() { setState(() {
_changedHere = true; _changedHere = true;
_temperaturePending = true;
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1)); _tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
}); });
_tempThrottleTimer = Timer(Duration(seconds: 2), () { _tempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() { setState(() {
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"})); _temperaturePending = false;
_resetStateTimer(entity); ConnectionManager().callService(
domain: entity.domain,
service: "set_temperature",
entityId: entity.entityId,
data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}
);
}); });
}); });
} }
void _setTargetTemp(ClimateEntity entity) { void _setTargetTemp(ClimateEntity entity) {
if (_targetTempThrottleTimer!=null) { _targetTempThrottleTimer?.cancel();
_targetTempThrottleTimer.cancel();
}
setState(() { setState(() {
_changedHere = true; _changedHere = true;
_temperaturePending = true;
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1)); _tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1)); _tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
}); });
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () { _targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() { setState(() {
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"})); _temperaturePending = false;
_resetStateTimer(entity); ConnectionManager().callService(
domain: entity.domain,
service: "set_temperature",
entityId: entity.entityId,
data: {"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}
);
}); });
}); });
} }
@ -111,8 +119,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpTargetHumidity = value.roundToDouble(); _tmpTargetHumidity = value.roundToDouble();
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_humidity", entity.entityId,{"humidity": "$_tmpTargetHumidity"})); ConnectionManager().callService(
_resetStateTimer(entity); domain: entity.domain,
service: "set_humidity",
entityId: entity.entityId,
data: {"humidity": "$_tmpTargetHumidity"}
);
}); });
} }
@ -120,8 +132,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpHVACMode = value; _tmpHVACMode = value;
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_hvac_mode", entity.entityId,{"hvac_mode": "$_tmpHVACMode"})); ConnectionManager().callService(
_resetStateTimer(entity); domain: entity.domain,
service: "set_hvac_mode",
entityId: entity.entityId,
data: {"hvac_mode": "$_tmpHVACMode"}
);
}); });
} }
@ -129,8 +145,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpSwingMode = value; _tmpSwingMode = value;
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_swing_mode", entity.entityId,{"swing_mode": "$_tmpSwingMode"})); ConnectionManager().callService(
_resetStateTimer(entity); domain: entity.domain,
service: "set_swing_mode",
entityId: entity.entityId,
data: {"swing_mode": "$_tmpSwingMode"}
);
}); });
} }
@ -138,8 +158,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpFanMode = value; _tmpFanMode = value;
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_fan_mode", entity.entityId,{"fan_mode": "$_tmpFanMode"})); ConnectionManager().callService(domain: entity.domain, service: "set_fan_mode", entityId: entity.entityId, data: {"fan_mode": "$_tmpFanMode"});
_resetStateTimer(entity);
}); });
} }
@ -147,8 +166,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpPresetMode = value; _tmpPresetMode = value;
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_preset_mode", entity.entityId,{"preset_mode": "$_tmpPresetMode"})); ConnectionManager().callService(domain: entity.domain, service: "set_preset_mode", entityId: entity.entityId, data: {"preset_mode": "$_tmpPresetMode"});
_resetStateTimer(entity);
}); });
} }
@ -165,18 +183,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpAuxHeat = value; _tmpAuxHeat = value;
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"})); ConnectionManager().callService(domain: entity.domain, service: "set_aux_heat", entityId: entity.entityId, data: {"aux_heat": "$_tmpAuxHeat"});
_resetStateTimer(entity);
});
}
void _resetStateTimer(ClimateEntity entity) {
if (_resetTimer!=null) {
_resetTimer.cancel();
}
_resetTimer = Timer(Duration(seconds: 3), () {
setState(() {});
_resetVars(entity);
}); });
} }
@ -184,11 +191,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final entityModel = EntityModel.of(context); final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entityWrapper.entity; final ClimateEntity entity = entityModel.entityWrapper.entity;
Logger.d("[Climate widget build] changed here = $_changedHere");
if (_changedHere) { if (_changedHere) {
_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow); //_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
_changedHere = false; _changedHere = false;
} else { } else {
_resetTimer?.cancel();
_resetVars(entity); _resetVars(entity);
} }
return Padding( return Padding(
@ -296,7 +303,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
)), )),
TemperatureControlWidget( TemperatureControlWidget(
value: _tmpTemperature, value: _tmpTemperature,
fontColor: _showPending ? Colors.red : Colors.black, fontColor: _temperaturePending ? Colors.red : Colors.black,
onDec: () => _temperatureDown(entity), onDec: () => _temperatureDown(entity),
onInc: () => _temperatureUp(entity), onInc: () => _temperatureUp(entity),
) )
@ -313,7 +320,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
controls.addAll(<Widget>[ controls.addAll(<Widget>[
TemperatureControlWidget( TemperatureControlWidget(
value: _tmpTargetLow, value: _tmpTargetLow,
fontColor: _showPending ? Colors.red : Colors.black, fontColor: _temperaturePending ? Colors.red : Colors.black,
onDec: () => _targetLowDown(entity), onDec: () => _targetLowDown(entity),
onInc: () => _targetLowUp(entity), onInc: () => _targetLowUp(entity),
), ),
@ -326,7 +333,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
controls.add( controls.add(
TemperatureControlWidget( TemperatureControlWidget(
value: _tmpTargetHigh, value: _tmpTargetHigh,
fontColor: _showPending ? Colors.red : Colors.black, fontColor: _temperaturePending ? Colors.red : Colors.black,
onDec: () => _targetHighDown(entity), onDec: () => _targetHighDown(entity),
onInc: () => _targetHighUp(entity), onInc: () => _targetHighUp(entity),
) )
@ -404,7 +411,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
@override @override
void dispose() { void dispose() {
_resetTimer?.cancel();
super.dispose(); super.dispose();
} }

View File

@ -3,7 +3,7 @@ part of '../../../main.dart';
class ModeSelectorWidget extends StatelessWidget { class ModeSelectorWidget extends StatelessWidget {
final String caption; final String caption;
final List<String> options; final List options;
final String value; final String value;
final double captionFontSize; final double captionFontSize;
final double valueFontSize; final double valueFontSize;
@ -45,10 +45,10 @@ class ModeSelectorWidget extends StatelessWidget {
color: Colors.black, color: Colors.black,
), ),
hint: Text("Select ${caption.toLowerCase()}"), hint: Text("Select ${caption.toLowerCase()}"),
items: options.map((String value) { items: options.map((value) {
return new DropdownMenuItem<String>( return new DropdownMenuItem<String>(
value: value, value: '$value',
child: Text(value), child: Text('$value'),
); );
}).toList(), }).toList(),
onChanged: (mode) => onChange(mode), onChanged: (mode) => onChange(mode),

View File

@ -18,7 +18,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
setState(() { setState(() {
_tmpPosition = position.roundToDouble(); _tmpPosition = position.roundToDouble();
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()})); ConnectionManager().callService(domain: entity.domain, service: "set_cover_position", entityId: entity.entityId, data: {"position": _tmpPosition.round()});
}); });
} }
@ -26,7 +26,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
setState(() { setState(() {
_tmpTiltPosition = position.roundToDouble(); _tmpTiltPosition = position.roundToDouble();
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()})); ConnectionManager().callService(domain: entity.domain, service: "set_cover_tilt_position", entityId: entity.entityId, data: {"tilt_position": _tmpTiltPosition.round()});
}); });
} }
@ -135,18 +135,18 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
class CoverTiltControlsWidget extends StatelessWidget { class CoverTiltControlsWidget extends StatelessWidget {
void _open(CoverEntity entity) { void _open(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "open_cover_tilt", entity.entityId, null)); domain: entity.domain, service: "open_cover_tilt", entityId: entity.entityId);
} }
void _close(CoverEntity entity) { void _close(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "close_cover_tilt", entity.entityId, null)); domain: entity.domain, service: "close_cover_tilt", entityId: entity.entityId);
} }
void _stop(CoverEntity entity) { void _stop(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "stop_cover_tilt", entity.entityId, null)); domain: entity.domain, service: "stop_cover_tilt", entityId: entity.entityId);
} }
@override @override

View File

@ -2,18 +2,27 @@ part of '../../../main.dart';
class CoverStateWidget extends StatelessWidget { class CoverStateWidget extends StatelessWidget {
void _open(CoverEntity entity) { void _open(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "open_cover", entity.entityId, null)); domain: entity.domain,
service: "open_cover",
entityId: entity.entityId
);
} }
void _close(CoverEntity entity) { void _close(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "close_cover", entity.entityId, null)); domain: entity.domain,
service: "close_cover",
entityId: entity.entityId
);
} }
void _stop(CoverEntity entity) { void _stop(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "stop_cover", entity.entityId, null)); domain: entity.domain,
service: "stop_cover",
entityId: entity.entityId
);
} }
@override @override

View File

@ -35,8 +35,7 @@ class DateTimeEntity extends Entity {
return formattedState; return formattedState;
} }
void setNewState(newValue) { void setNewState(Map newValue) {
eventBus ConnectionManager().callService(domain: domain, service: "set_datetime", entityId: entityId, data: newValue);
.fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue));
} }
} }

View File

@ -61,6 +61,11 @@ class DefaultEntityContainer extends StatelessWidget {
entityModel.entityWrapper.handleTap(); entityModel.entityWrapper.handleTap();
} }
}, },
onDoubleTap: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleDoubleTap();
}
},
child: result, child: result,
); );
} else { } else {

View File

@ -153,7 +153,7 @@ class Entity {
domain = rawData["entity_id"].split(".")[0]; domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"]; entityId = rawData["entity_id"];
deviceClass = attributes["device_class"]; deviceClass = attributes["device_class"];
state = rawData["state"]; state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state); displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
_lastUpdated = DateTime.tryParse(rawData["last_updated"]); _lastUpdated = DateTime.tryParse(rawData["last_updated"]);
entityPicture = _getEntityPictureUrl(webHost); entityPicture = _getEntityPictureUrl(webHost);
@ -221,7 +221,7 @@ class Entity {
String getAttribute(String attributeName) { String getAttribute(String attributeName) {
if (attributes != null) { if (attributes != null) {
return attributes["$attributeName"]; return attributes["$attributeName"].toString();
} }
return null; return null;
} }

View File

@ -17,7 +17,7 @@ class EntityPageLayout extends StatelessWidget {
showClose ? showClose ?
Container( Container(
color: Colors.blue[300], color: Colors.blue[300],
height: 36, height: 40,
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
@ -37,7 +37,7 @@ class EntityPageLayout extends StatelessWidget {
padding: EdgeInsets.all(0), padding: EdgeInsets.all(0),
icon: Icon(Icons.close), icon: Icon(Icons.close),
color: Colors.white, color: Colors.white,
iconSize: 30.0, iconSize: 36.0,
onPressed: () { onPressed: () {
eventBus.fire(ShowEntityPageEvent()); eventBus.fire(ShowEntityPageEvent());
}, },

View 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
),
);
}
}

View File

@ -2,47 +2,46 @@ part of '../main.dart';
class EntityWrapper { class EntityWrapper {
String displayName; String overrideName;
String icon; final String overrideIcon;
String unitOfMeasurement;
String entityPicture;
EntityUIAction uiAction; EntityUIAction uiAction;
Entity entity; Entity entity;
String unitOfMeasurementOverride;
final List stateFilter;
String get icon => overrideIcon ?? entity.icon;
String get entityPicture => entity.entityPicture;
String get displayName => overrideName ?? entity.displayName;
String get unitOfMeasurement => unitOfMeasurementOverride ?? entity.unitOfMeasurement;
EntityWrapper({ EntityWrapper({
this.entity, this.entity,
String icon, this.overrideIcon,
String displayName, this.overrideName,
this.uiAction this.uiAction,
this.stateFilter
}) { }) {
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) { 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) { if (uiAction == null) {
uiAction = EntityUIAction(); uiAction = EntityUIAction();
} }
unitOfMeasurement = entity.unitOfMeasurement;
} }
} }
void handleTap() { void handleTap() {
switch (uiAction.tapAction) { switch (uiAction.tapAction) {
case EntityUIAction.toggle: { case EntityUIAction.toggle: {
eventBus.fire( ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break; break;
} }
case EntityUIAction.callService: { case EntityUIAction.callService: {
if (uiAction.tapService != null) { if (uiAction.tapService != null) {
eventBus.fire( ConnectionManager().callService(
ServiceCallEvent(uiAction.tapService.split(".")[0], domain: uiAction.tapService.split(".")[0],
uiAction.tapService.split(".")[1], null, service: uiAction.tapService.split(".")[1],
uiAction.tapServiceData)); data: uiAction.tapServiceData
);
} }
break; break;
} }
@ -58,7 +57,7 @@ class EntityWrapper {
} }
case EntityUIAction.navigate: { case EntityUIAction.navigate: {
if (uiAction.tapService.startsWith("/")) { if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
//TODO handle local urls //TODO handle local urls
Logger.w("Local urls is not supported yet"); Logger.w("Local urls is not supported yet");
} else { } else {
@ -76,17 +75,17 @@ class EntityWrapper {
void handleHold() { void handleHold() {
switch (uiAction.holdAction) { switch (uiAction.holdAction) {
case EntityUIAction.toggle: { case EntityUIAction.toggle: {
eventBus.fire( ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break; break;
} }
case EntityUIAction.callService: { case EntityUIAction.callService: {
if (uiAction.holdService != null) { if (uiAction.holdService != null) {
eventBus.fire( ConnectionManager().callService(
ServiceCallEvent(uiAction.holdService.split(".")[0], domain: uiAction.holdService.split(".")[0],
uiAction.holdService.split(".")[1], null, service: uiAction.holdService.split(".")[1],
uiAction.holdServiceData)); data: uiAction.holdServiceData
);
} }
break; break;
} }
@ -98,7 +97,7 @@ class EntityWrapper {
} }
case EntityUIAction.navigate: { case EntityUIAction.navigate: {
if (uiAction.holdService.startsWith("/")) { if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
//TODO handle local urls //TODO handle local urls
Logger.w("Local urls is not supported yet"); Logger.w("Local urls is not supported yet");
} else { } else {
@ -113,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;
}
}
}
} }

View File

@ -24,9 +24,12 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
setState(() { setState(() {
_tmpOscillate = oscillate; _tmpOscillate = oscillate;
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
"fan", "oscillate", entity.entityId, domain: "fan",
{"oscillating": oscillate})); service: "oscillate",
entityId: entity.entityId,
data: {"oscillating": oscillate}
);
}); });
} }
@ -34,9 +37,12 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
setState(() { setState(() {
_tmpDirectionForward = forward; _tmpDirectionForward = forward;
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
"fan", "set_direction", entity.entityId, domain: "fan",
{"direction": forward ? "forward" : "reverse"})); service: "set_direction",
entityId: entity.entityId,
data: {"direction": forward ? "forward" : "reverse"}
);
}); });
} }
@ -44,9 +50,12 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
setState(() { setState(() {
_tmpSpeed = value; _tmpSpeed = value;
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
"fan", "set_speed", entity.entityId, domain: "fan",
{"speed": value})); service: "set_speed",
entityId: entity.entityId,
data: {"speed": value}
);
}); });
} }

View File

@ -18,7 +18,7 @@ class FlatServiceButton extends StatelessWidget {
}) : super(key: key); }) : super(key: key);
void _setNewState() { void _setNewState() {
eventBus.fire(new ServiceCallEvent(serviceDomain, serviceName, entityId, null)); ConnectionManager().callService(domain: serviceDomain, service: serviceName, entityId: entityId);
} }
@override @override

View File

@ -28,9 +28,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() { setState(() {
_tmpBrightness = value.round(); _tmpBrightness = value.round();
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId, domain: entity.domain,
{"brightness": _tmpBrightness})); service: "turn_on",
entityId: entity.entityId,
data: {"brightness": _tmpBrightness}
);
}); });
} }
@ -38,9 +41,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() { setState(() {
_tmpWhiteValue = value.round(); _tmpWhiteValue = value.round();
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId, domain: entity.domain,
{"white_value": _tmpWhiteValue})); service: "turn_on",
entityId: entity.entityId,
data: {"white_value": _tmpWhiteValue}
);
}); });
} }
@ -49,9 +55,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() { setState(() {
_tmpColorTemp = value.round(); _tmpColorTemp = value.round();
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId, domain: entity.domain,
{"color_temp": _tmpColorTemp})); service: "turn_on",
entityId: entity.entityId,
data: {"color_temp": _tmpColorTemp}
);
}); });
} }
@ -59,10 +68,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() { setState(() {
_tmpColor = color; _tmpColor = color;
_changedHere = true; _changedHere = true;
Logger.d( "HS Color: [${color.hue}, ${color.saturation}]"); ConnectionManager().callService(
eventBus.fire(new ServiceCallEvent( domain: entity.domain,
entity.domain, "turn_on", entity.entityId, service: "turn_on",
{"hs_color": [color.hue, color.saturation*100]})); entityId: entity.entityId,
data: {"hs_color": [color.hue, color.saturation*100]}
);
}); });
} }
@ -71,9 +82,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpEffect = value; _tmpEffect = value;
_changedHere = true; _changedHere = true;
if (_tmpEffect != null) { if (_tmpEffect != null) {
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId, domain: entity.domain,
{"effect": "$value"})); service: "turn_on",
entityId: entity.entityId,
data: {"effect": "$value"}
);
} }
}); });
} }
@ -227,8 +241,6 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
Widget _buildEffectControl(LightEntity entity) { Widget _buildEffectControl(LightEntity entity) {
if ((entity.supportEffect) && (entity.effectList != null)) { if ((entity.supportEffect) && (entity.effectList != null)) {
Logger.d("[LIGHT] entity effects: ${entity.effectList}");
Logger.d("[LIGHT] current effect: $_tmpEffect");
List<String> list = List.from(entity.effectList); List<String> list = List.from(entity.effectList);
if (_tmpEffect!= null && !list.contains(_tmpEffect)) { if (_tmpEffect!= null && !list.contains(_tmpEffect)) {
list.insert(0, _tmpEffect); list.insert(0, _tmpEffect);

View File

@ -7,11 +7,11 @@ class LockStateWidget extends StatelessWidget {
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key); const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
void _lock(Entity entity) { void _lock(Entity entity) {
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null)); ConnectionManager().callService(domain: "lock", service: "lock", entityId: entity.entityId);
} }
void _unlock(Entity entity) { void _unlock(Entity entity) {
eventBus.fire(new ServiceCallEvent("lock", "unlock", entity.entityId, null)); ConnectionManager().callService(domain: "lock", service: "unlock", entityId: entity.entityId);
} }
@override @override

View File

@ -84,25 +84,22 @@ class MediaPlayerEntity extends Entity {
} }
bool canCalculateActualPosition() { bool canCalculateActualPosition() {
return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds >= 0; return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds > 0;
} }
double getActualPosition() { double getActualPosition() {
double result = 0; double result = 0;
if (canCalculateActualPosition()) { Duration durationD;
Duration durationD; Duration positionD;
Duration positionD; durationD = Duration(seconds: durationSeconds);
durationD = Duration(seconds: durationSeconds); positionD = Duration(
positionD = Duration(
seconds: positionSeconds); seconds: positionSeconds);
result = positionD.inSeconds.toDouble(); result = positionD.inSeconds.toDouble();
int differenceInSeconds = DateTime int differenceInSeconds = DateTime
.now() .now()
.difference(positionLastUpdated) .difference(positionLastUpdated)
.inSeconds; .inSeconds;
result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble(); result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble();
}
return result; return result;
} }

View File

@ -22,13 +22,13 @@ class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context); final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity; final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
double progress; double progress = 0;
int currentPosition; int currentPosition;
if (entity.canCalculateActualPosition()) { if (entity.canCalculateActualPosition()) {
currentPosition = entity.getActualPosition().toInt(); currentPosition = entity.getActualPosition().toInt();
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100; if (currentPosition > 0) {
} else { progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
progress = 0; }
} }
return LinearProgressIndicator( return LinearProgressIndicator(
value: progress, value: progress,

View File

@ -56,12 +56,12 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
color: Colors.orange, color: Colors.orange,
focusColor: Colors.white, focusColor: Colors.white,
onPressed: () { onPressed: () {
eventBus.fire(ServiceCallEvent( ConnectionManager().callService(
"media_player", domain: "media_player",
"media_seek", service: "media_seek",
"${entity.entityId}", entityId: entity.entityId,
{"seek_position": _savedPosition} data: {"seek_position": _savedPosition}
)); );
setState(() { setState(() {
_savedPosition = 0; _savedPosition = 0;
}); });
@ -103,12 +103,12 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
_seekStarted = false; _seekStarted = false;
Timer(Duration(milliseconds: 500), () { Timer(Duration(milliseconds: 500), () {
if (!_seekStarted) { if (!_seekStarted) {
eventBus.fire(ServiceCallEvent( ConnectionManager().callService(
"media_player", domain: "media_player",
"media_seek", service: "media_seek",
"${entity.entityId}", entityId: entity.entityId,
{"seek_position": val} data: {"seek_position": val}
)); );
setState(() { setState(() {
_changedHere = true; _changedHere = true;
_currentPosition = val; _currentPosition = val;

View File

@ -1,7 +1,7 @@
part of '../../../main.dart'; part of '../../../main.dart';
class MediaPlayerWidget extends StatelessWidget { class MediaPlayerWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context); final EntityModel entityModel = EntityModel.of(context);
@ -118,26 +118,28 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
void _setPower(MediaPlayerEntity entity) { void _setPower(MediaPlayerEntity entity) {
if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) {
if (entity.state == EntityState.off) { if (entity.state == EntityState.off) {
Logger.d("${entity.entityId} turn_on"); ConnectionManager().callService(
eventBus.fire(new ServiceCallEvent( domain: entity.domain,
entity.domain, "turn_on", entity.entityId, service: "turn_on",
null)); entityId: entity.entityId
);
} else { } else {
Logger.d("${entity.entityId} turn_off"); ConnectionManager().callService(
eventBus.fire(new ServiceCallEvent( domain: entity.domain,
entity.domain, "turn_off", entity.entityId, service: "turn_off",
null)); entityId: entity.entityId
);
} }
}
} }
void _callAction(MediaPlayerEntity entity, String action) { void _callAction(MediaPlayerEntity entity, String action) {
Logger.d("${entity.entityId} $action"); Logger.d("${entity.entityId} $action");
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "$action", entity.entityId, domain: entity.domain,
null)); service: "$action",
entityId: entity.entityId
);
} }
@override @override
@ -264,27 +266,50 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
setState(() { setState(() {
_changedHere = true; _changedHere = true;
_newVolumeLevel = value; _newVolumeLevel = value;
eventBus.fire(ServiceCallEvent("media_player", "volume_set", entityId, {"volume_level": value})); ConnectionManager().callService(
domain: "media_player",
service: "volume_set",
entityId: entityId,
data: {"volume_level": value}
);
}); });
} }
void _setVolumeMute(bool isMuted, String entityId) { void _setVolumeMute(bool isMuted, String entityId) {
eventBus.fire(ServiceCallEvent("media_player", "volume_mute", entityId, {"is_volume_muted": isMuted})); ConnectionManager().callService(
domain: "media_player",
service: "volume_mute",
entityId: entityId,
data: {"is_volume_muted": isMuted}
);
} }
void _setVolumeUp(String entityId) { void _setVolumeUp(String entityId) {
eventBus.fire(ServiceCallEvent("media_player", "volume_up", entityId, null)); ConnectionManager().callService(
domain: "media_player",
service: "volume_up",
entityId: entityId
);
} }
void _setVolumeDown(String entityId) { void _setVolumeDown(String entityId) {
eventBus.fire(ServiceCallEvent("media_player", "volume_down", entityId, null)); ConnectionManager().callService(
domain: "media_player",
service: "volume_down",
entityId: entityId
);
} }
void _setSoundMode(String value, String entityId) { void _setSoundMode(String value, String entityId) {
setState(() { setState(() {
_newSoundMode = value; _newSoundMode = value;
_changedHere = true; _changedHere = true;
eventBus.fire(ServiceCallEvent("media_player", "select_sound_mode", entityId, {"sound_mode": "$value"})); ConnectionManager().callService(
domain: "media_player",
service: "select_sound_mode",
entityId: entityId,
data: {"sound_mode": "$value"}
);
}); });
} }
@ -292,7 +317,12 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
setState(() { setState(() {
_newSource = source; _newSource = source;
_changedHere = true; _changedHere = true;
eventBus.fire(ServiceCallEvent("media_player", "select_source", entityId, {"source": "$source"})); ConnectionManager().callService(
domain: "media_player",
service: "select_source",
entityId: entityId,
data: {"source": "$source"}
);
}); });
} }
@ -326,13 +356,13 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
volumeStepWidget = Row( volumeStepWidget = Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
onPressed: () => _setVolumeUp(entity.entityId)
),
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")), icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
onPressed: () => _setVolumeDown(entity.entityId) onPressed: () => _setVolumeDown(entity.entityId)
),
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
onPressed: () => _setVolumeUp(entity.entityId)
) )
], ],
); );
@ -430,15 +460,15 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
} }
void _duplicateTo(entity) { void _duplicateTo(entity) {
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt(); if (entity.canCalculateActualPosition()) {
if (MediaQuery.of(context).size.width < Sizes.tabletMinWidth) { HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
Navigator.of(context).popAndPushNamed("/play-media", arguments: {"url": entity.attributes["media_content_id"], "type": entity.attributes["media_content_type"]});
} else { } else {
Navigator.of(context).pushNamed("/play-media", arguments: { HomeAssistant().savedPlayerPosition = 0;
}
Navigator.of(context).pushNamed("/play-media", arguments: {
"url": entity.attributes["media_content_id"], "url": entity.attributes["media_content_id"],
"type": entity.attributes["media_content_type"] "type": entity.attributes["media_content_type"]
}); });
}
} }
void _switchTo(entity) { void _switchTo(entity) {

View File

@ -11,8 +11,12 @@ class SelectStateWidget extends StatefulWidget {
class _SelectStateWidgetState extends State<SelectStateWidget> { class _SelectStateWidgetState extends State<SelectStateWidget> {
void setNewState(domain, entityId, newValue) { void setNewState(domain, entityId, newValue) {
eventBus.fire(new ServiceCallEvent(domain, "select_option", entityId, ConnectionManager().callService(
{"option": "$newValue"})); domain: domain,
service: "select_option",
entityId: entityId,
data: {"option": "$newValue"}
);
} }
@override @override

View File

@ -18,8 +18,12 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
_newValue = newValue; _newValue = newValue;
_changedHere = true; _changedHere = true;
}); });
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId, ConnectionManager().callService(
{"value": "${newValue.toString()}"})); domain: domain,
service: "set_value",
entityId: entityId,
data: {"value": "${newValue.toString()}"}
);
} }
@override @override

View File

@ -38,8 +38,11 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
} else { } else {
domain = entity.domain; domain = entity.domain;
} }
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null)); domain: domain,
service: (newValue as bool) ? "turn_on" : "turn_off",
entityId: entity.entityId
);
} }
@override @override

View File

@ -26,8 +26,12 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
void setNewState(newValue, domain, entityId) { void setNewState(newValue, domain, entityId) {
if (validate(newValue, _minLength, _maxLength)) { if (validate(newValue, _minLength, _maxLength)) {
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId, ConnectionManager().callService(
{"value": "$newValue"})); domain: domain,
service: "set_value",
entityId: entityId,
data: {"value": "$newValue"}
);
} else { } else {
setState(() { setState(() {
_tmpValue = _entityState; _tmpValue = _entityState;

View File

@ -197,7 +197,7 @@ class VacuumControls extends StatelessWidget {
domain: "vacuum", domain: "vacuum",
entityId: entity.entityId, entityId: entity.entityId,
service: "set_fan_speed", service: "set_fan_speed",
additionalServiceData: {"fan_speed": val} data: {"fan_speed": val}
) )
), ),
); );

View File

@ -152,39 +152,12 @@ class EntityCollection {
return _allEntities[entityId] != null; return _allEntities[entityId] != null;
} }
List<Entity> getByDomains({List<String> domains, List<String> stateFiler}) { List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) {
return _allEntities.values.where((entity) { return _allEntities.values.where((entity) {
return domains.contains(entity.domain) && return
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null); (excludeDomains.isEmpty || !excludeDomains.contains(entity.domain)) &&
(includeDomains.isEmpty || includeDomains.contains(entity.domain)) &&
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
}).toList(); }).toList();
} }
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;
}
} }

View File

@ -2,6 +2,8 @@ part of 'main.dart';
class HomeAssistant { class HomeAssistant {
static const DEFAULT_DASHBOARD = 'lovelace';
static final HomeAssistant _instance = HomeAssistant._internal(); static final HomeAssistant _instance = HomeAssistant._internal();
factory HomeAssistant() { factory HomeAssistant() {
@ -11,27 +13,33 @@ class HomeAssistant {
EntityCollection entities; EntityCollection entities;
HomeAssistantUI ui; HomeAssistantUI ui;
Map _instanceConfig = {}; Map _instanceConfig = {};
Map services;
String _userName; String _userName;
bool childMode; String _lovelaceDashbordUrl;
HSVColor savedColor; HSVColor savedColor;
int savedPlayerPosition; int savedPlayerPosition;
String sendToPlayerId; String sendToPlayerId;
String sendFromPlayerId; String sendFromPlayerId;
Map services;
bool autoUi = false;
String fcmToken; String fcmToken;
Map _rawLovelaceData; Map _rawLovelaceData;
var _rawStates;
var _rawUserInfo;
var _rawPanels;
set lovelaceDashboardUrl(String val) => _lovelaceDashbordUrl = val;
List<Panel> panels = []; List<Panel> panels = [];
Duration fetchTimeout = Duration(seconds: 30); Duration fetchTimeout = Duration(seconds: 30);
String get locationName { String get locationName {
if (ConnectionManager().useLovelace) { if (!autoUi) {
return ui?.title ?? ""; return ui?.title ?? "Home";
} else { } else {
return _instanceConfig["location_name"] ?? ""; return _instanceConfig["location_name"] ?? "Home";
} }
} }
String get userName => _userName ?? locationName; String get userName => _userName ?? locationName;
@ -42,38 +50,37 @@ class HomeAssistant {
HomeAssistant._internal() { HomeAssistant._internal() {
ConnectionManager().onStateChangeCallback = _handleEntityStateChange; ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
ConnectionManager().onLovelaceUpdatedCallback = _handleLovelaceUpdate;
DeviceInfoManager().loadDeviceInfo(); DeviceInfoManager().loadDeviceInfo();
} }
Completer _fetchCompleter; Completer _fetchCompleter;
Future fetchData() { Future fetchData(bool uiOnly) {
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) { if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
Logger.w("Previous data fetch is not completed yet"); Logger.w("Previous data fetch is not completed yet");
return _fetchCompleter.future; return _fetchCompleter.future;
} }
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
_fetchCompleter = Completer(); _fetchCompleter = Completer();
List<Future> futures = []; List<Future> futures = [];
futures.add(_getStates()); if (!uiOnly) {
if (ConnectionManager().useLovelace) { if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
futures.add(_getLovelace()); futures.add(_getStates(null));
futures.add(_getConfig(null));
futures.add(_getUserInfo(null));
futures.add(_getPanels(null));
futures.add(_getServices(null));
}
if (!autoUi) {
futures.add(_getLovelace(null));
} }
futures.add(_getConfig());
futures.add(_getServices());
futures.add(_getUserInfo());
futures.add(_getPanels());
futures.add(ConnectionManager().sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "state_changed"},
));
Future.wait(futures).then((_) { Future.wait(futures).then((_) {
if (isMobileAppEnabled) { if (isMobileAppEnabled) {
if (!childMode) _createUI(); _createUI();
_fetchCompleter.complete(); _fetchCompleter.complete();
MobileAppIntegrationManager.checkAppRegistration(); if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
} else { } else {
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app-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) { }).catchError((e) {
_fetchCompleter.completeError(e); _fetchCompleter.completeError(e);
@ -81,6 +88,48 @@ class HomeAssistant {
return _fetchCompleter.future; return _fetchCompleter.future;
} }
Future<void> fetchDataFromCache() async {
Logger.d('Loading cached data');
SharedPreferences prefs = await SharedPreferences.getInstance();
bool cached = prefs.getBool('cached');
if (cached != null && cached) {
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
try {
_getStates(prefs);
if (!autoUi) {
_getLovelace(prefs);
}
_getConfig(prefs);
_getUserInfo(prefs);
_getPanels(prefs);
_getServices(prefs);
if (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 { Future logout() async {
Logger.d("Logging out..."); Logger.d("Logging out...");
await ConnectionManager().logout().then((_) { await ConnectionManager().logout().then((_) {
@ -90,71 +139,181 @@ class HomeAssistant {
}); });
} }
Future _getConfig() async { Future _getConfig(SharedPreferences sharedPrefs) async {
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) { if (sharedPrefs != null) {
_instanceConfig = Map.from(data); try {
}).catchError((e) { var data = json.decode(sharedPrefs.getString('cached_config'));
throw HAError("Error getting config: ${e}"); _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) {
await ConnectionManager().sendSocketMessage(type: "get_states").then( _instanceConfig = Map.from(data);
(data) => entities.parse(data)
).catchError((e) {
throw HAError("Error getting states: $e");
});
} }
Future _getLovelace() async { Future _getStates(SharedPreferences sharedPrefs) async {
await ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) { if (sharedPrefs != null) {
throw HAError("Error getting lovelace config: $e"); try {
}); var data = json.decode(sharedPrefs.getString('cached_states'));
_parseStates(data);
} catch (e) {
throw HAError("Error getting states: $e");
}
} else {
await ConnectionManager().sendSocketMessage(type: "get_states").then(
(data) => _parseStates(data)
).catchError((e) {
throw HAError("Error getting states: $e");
});
}
} }
Future _getUserInfo() async { void _parseStates(data) {
_rawStates = data;
entities.parse(data);
}
Future _getLovelace(SharedPreferences sharedPrefs) {
if (sharedPrefs != null) {
try {
var data = json.decode(sharedPrefs.getString('cached_lovelace'));
_rawLovelaceData = data;
} catch (e) {
autoUi = true;
}
return Future.value();
} else {
Completer completer = Completer();
var additionalData;
if (_lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
additionalData = {
'url_path': _lovelaceDashbordUrl
};
}
ConnectionManager().sendSocketMessage(
type: 'lovelace/config',
additionalData: additionalData
).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; _userName = null;
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) { await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
_userName = data["name"];
childMode = _userName.startsWith("[child]");
}).catchError((e) {
Logger.w("Can't get user info: $e"); Logger.w("Can't get user info: $e");
}); });
} }
Future _getServices() async { void _parseUserInfo(data) {
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) { _rawUserInfo = data;
Logger.d("Got ${data.length} services"); _userName = data["name"];
Logger.d("Media extractor: ${data["media_extractor"]}");
services = data;
}).catchError((e) {
Logger.w("Can't get services: $e");
});
} }
Future _getPanels() async { Future _getPanels(SharedPreferences sharedPrefs) async {
panels.clear(); if (sharedPrefs != null) {
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) { try {
data.forEach((k,v) { var data = json.decode(sharedPrefs.getString('cached_panels'));
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}"; _parsePanels(data);
panels.add(Panel( } catch (e) {
id: k, throw HAError("Error getting panels list: $e");
type: v["component_name"], }
title: title, } else {
urlPath: v["url_path"], await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) => _parsePanels(data)).catchError((e) {
config: v["config"], throw HAError("Error getting panels list: $e");
icon: v["icon"]
)
);
}); });
}).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) { void _handleEntityStateChange(Map eventData) {
//TheLogger.debug( "New state for ${eventData['entity_id']}"); //TheLogger.debug( "New state for ${eventData['entity_id']}");
if (_fetchCompleter.isCompleted) { if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
Map data = Map.from(eventData); Map data = Map.from(eventData);
eventBus.fire(new StateChangedEvent( eventBus.fire(new StateChangedEvent(
entityId: data["entity_id"], entityId: data["entity_id"],
@ -163,204 +322,27 @@ class HomeAssistant {
} }
} }
void _parseLovelace() { bool isServiceExist(String service) {
Logger.d("--Title: ${_rawLovelaceData["title"]}"); return services != null &&
ui.title = _rawLovelaceData["title"]; services.isNotEmpty &&
int viewCounter = 0; services.containsKey(service);
Logger.d("--Views count: ${_rawLovelaceData['views'].length}");
_rawLovelaceData["views"].forEach((rawView){
Logger.d("----view id: ${rawView['id']}");
HAView view = HAView(
count: viewCounter,
id: "${rawView['id']}",
name: rawView['title'],
iconName: rawView['icon'],
panel: rawView['panel'] ?? false,
);
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'],
min: rawCardInfo['min'] ?? 0,
max: rawCardInfo['max'] ?? 100,
unit: rawCardInfo['unit'],
severity: rawCardInfo['severity']
);
if (rawCardInfo["cards"] != null) {
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
}
var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
rawEntities?.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"])));
}
}
});
var rawSingleEntity = rawCard["entity"] ?? rawCardInfo["entity"];
if (rawSingleEntity != null) {
var en = rawSingleEntity;
if (en is String) {
if (entities.isExist(en)) {
Entity e = entities.get(en);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
icon: rawCardInfo["icon"],
displayName: rawCardInfo["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
}
} else {
if (entities.isExist(en["entity"])) {
Entity e = entities.get(en["entity"]);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
icon: en["icon"],
displayName: en["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
}
}
}
result.add(card);
} catch (e) {
Logger.e("There was an error parsing card: ${e.toString()}");
}
});
return result;
} }
void _createUI() { void _createUI() {
ui = HomeAssistantUI(); Logger.d("Creating Lovelace UI");
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) { ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData);
Logger.d("Creating Lovelace UI"); if (isServiceExist('zha_map')) {
_parseLovelace(); panels.add(
} else { Panel(
Logger.d("Creating group-based UI"); id: 'haclient_zha',
int viewCounter = 0; componentName: 'haclient_zha',
if (!entities.hasDefaultView) { title: 'ZHA',
HAView view = HAView( urlPath: '/haclient_zha',
count: viewCounter, icon: 'mdi:zigbee'
id: "group.default_view", )
name: "Home",
childEntities: entities.filterEntitiesForDefaultView()
); );
ui.views.add(
view
);
viewCounter += 1;
}
entities.viewEntities.forEach((viewEntity) {
HAView view = HAView(
count: viewCounter,
id: viewEntity.entityId,
name: viewEntity.displayName,
childEntities: viewEntity.childEntities
);
view.linkedEntity = viewEntity;
ui.views.add(
view
);
viewCounter += 1;
});
} }
} }
Widget buildViews(BuildContext context, TabController tabController) {
return ui.build(context, tabController);
}
} }
/* /*

View File

@ -1,6 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -8,6 +10,7 @@ import 'package:web_socket_channel/io.dart';
import 'package:event_bus/event_bus.dart'; import 'package:event_bus/event_bus.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:path_provider/path_provider.dart';
import 'package:url_launcher/url_launcher.dart' as urlLauncher; import 'package:url_launcher/url_launcher.dart' as urlLauncher;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:date_format/date_format.dart'; import 'package:date_format/date_format.dart';
@ -22,14 +25,15 @@ import 'package:device_info/device_info.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:in_app_purchase/in_app_purchase.dart';
import 'plugins/circular_slider/single_circular_slider.dart'; import 'plugins/circular_slider/single_circular_slider.dart';
import 'package:share/receive_share_state.dart';
import 'package:share/share.dart';
import 'plugins/dynamic_multi_column_layout.dart'; import 'plugins/dynamic_multi_column_layout.dart';
import 'plugins/spoiler_card.dart'; import 'plugins/spoiler_card.dart';
import 'package:uni_links/uni_links.dart';
import 'package:workmanager/workmanager.dart' as workManager; import 'package:workmanager/workmanager.dart' as workManager;
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:battery/battery.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'; import 'utils/logger.dart';
@ -84,6 +88,7 @@ part 'entities/slider/widgets/slider_controls.dart';
part 'entities/text/widgets/text_input_state.dart'; part 'entities/text/widgets/text_input_state.dart';
part 'entities/select/widgets/select_state.dart'; part 'entities/select/widgets/select_state.dart';
part 'entities/simple_state.widget.dart'; part 'entities/simple_state.widget.dart';
part 'entities/entity_picture.widget.dart';
part 'entities/timer/widgets/timer_state.dart'; part 'entities/timer/widgets/timer_state.dart';
part 'entities/climate/widgets/climate_state.widget.dart'; part 'entities/climate/widgets/climate_state.widget.dart';
part 'entities/cover/widgets/cover_state.dart'; part 'entities/cover/widgets/cover_state.dart';
@ -105,8 +110,9 @@ part 'pages/widgets/product_purchase.widget.dart';
part 'pages/widgets/page_loading_indicator.dart'; part 'pages/widgets/page_loading_indicator.dart';
part 'pages/widgets/page_loading_error.dart'; part 'pages/widgets/page_loading_error.dart';
part 'pages/panel.page.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/integration_settings.page.dart';
part 'pages/zha_page.dart';
part 'home_assistant.class.dart'; part 'home_assistant.class.dart';
part 'pages/log.page.dart'; part 'pages/log.page.dart';
part 'pages/entity.page.dart'; part 'pages/entity.page.dart';
@ -136,40 +142,84 @@ 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_seek_bar.widget.dart';
part 'entities/media_player/widgets/media_player_progress_bar.widget.dart'; part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
part 'pages/whats_new.page.dart'; part 'pages/whats_new.page.dart';
part 'pages/fullscreen.page.dart';
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = "HA Client"; const String appName = "HA Client";
const appVersionNumber = "0.7.0"; const appVersionNumber = "0.8.0";
const appVersionAdd = ""; const appVersionAdd = "";
const appVersion = "$appVersionNumber-$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 { void main() async {
FlutterError.onError = (errorDetails) { Crashlytics.instance.enableInDevMode = false;
Logger.e( "${errorDetails.exception}");
FlutterError.onError = (FlutterErrorDetails details) {
Logger.e(" Caut Flutter runtime error: ${details.exception}");
if (Logger.isInDebugMode) { if (Logger.isInDebugMode) {
FlutterError.dumpErrorToConsole(errorDetails); FlutterError.dumpErrorToConsole(details);
} }
Crashlytics.instance.recordFlutterError(details);
}; };
runZoned(() { runZoned(() {
workManager.Workmanager.initialize(
updateDeviceLocationIsolate,
isInDebugMode: false
);
runApp(new HAClientApp()); runApp(new HAClientApp());
}, onError: (error, stack) { }, onError: (error, stack) {
Logger.e("$error"); _reportError(error, stack);
Logger.e("$stack");
if (Logger.isInDebugMode) {
debugPrint("$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. // This widget is the root of your application.
@override @override
@ -190,8 +240,43 @@ class HAClientApp extends StatelessWidget {
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}", mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
), ),
"/log-view": (context) => LogViewPage(title: "Log"), "/log-view": (context) => LogViewPage(title: "Log"),
"/whats-new": (context) => WhatsNewPage() "/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.app/docs#authentication")
),
title: new Text("Login with HA"),
actions: <Widget>[
FlatButton(
child: Text("Manual", style: TextStyle(color: Colors.white)),
onPressed: () {
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
},
)
],
),
)
}, },
); );
} }
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}

View File

@ -9,46 +9,37 @@ class AuthManager {
} }
AuthManager._internal(); AuthManager._internal();
StreamSubscription deepLinksSubscription;
Future start({String oauthUrl}) { Future start({String oauthUrl}) {
Completer completer = Completer(); Completer completer = Completer();
deepLinksSubscription?.cancel(); final flutterWebviewPlugin = new standaloneWebview.FlutterWebviewPlugin();
deepLinksSubscription = getUriLinksStream().listen((Uri uri) { flutterWebviewPlugin.onUrlChanged.listen((String url) {
Logger.d("[LINKED AUTH] We got something private"); if (url.startsWith("https://ha-client.app/service/auth_callback.html")) {
_getTempToken(oauthUrl, uri.queryParameters["code"]) Logger.d("url=$url");
.then((tempToken) => completer.complete(tempToken)) String authCode = url.split("=")[1];
.catchError((_){ Logger.d("authCode=$authCode");
completer.completeError(HAError("Auth error")); Logger.d("We have auth code. Getting temporary access token...");
}); ConnectionManager().sendHTTPPost(
}, onError: (err) { endPoint: "/auth/token",
Logger.e("[LINKED AUTH] Error handling linked auth: $e"); contentType: "application/x-www-form-urlencoded",
completer.completeError(HAError("Auth error")); includeAuthHeader: false,
}); data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('https://ha-client.app')}"
Logger.d("Launching OAuth");
eventBus.fire(StartAuthEvent(oauthUrl, true));
return completer.future;
}
Future _getTempToken(String oauthUrl,String authCode) {
Completer completer = Completer();
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')}"
).then((response) { ).then((response) {
Logger.d("Got temp token"); Logger.d("Got temp token");
String tempToken = json.decode(response)['access_token']; String tempToken = json.decode(response)['access_token'];
Logger.d("Closing webview...");
eventBus.fire(StartAuthEvent(oauthUrl, false)); eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.complete(tempToken); completer.complete(tempToken);
}).catchError((e) { }).catchError((e) {
//flutterWebviewPlugin.close();
Logger.e("Error getting temp token: ${e.toString()}"); Logger.e("Error getting temp token: ${e.toString()}");
eventBus.fire(StartAuthEvent(oauthUrl, false)); eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.completeError(HAError("Error getting temp token")); completer.completeError(HAError("Error getting temp token"));
}); }).whenComplete(() => flutterWebviewPlugin.close());
}
});
Logger.d("Launching OAuth");
eventBus.fire(StartAuthEvent(oauthUrl, true));
return completer.future; return completer.future;
} }
} }

View File

@ -19,7 +19,6 @@ class ConnectionManager {
String _tempToken; String _tempToken;
String oauthUrl; String oauthUrl;
String webhookId; String webhookId;
bool useLovelace = true;
bool settingsLoaded = false; bool settingsLoaded = false;
bool get isAuthenticated => _token != null; bool get isAuthenticated => _token != null;
StreamSubscription _socketSubscription; StreamSubscription _socketSubscription;
@ -28,6 +27,7 @@ class ConnectionManager {
bool isConnected = false; bool isConnected = false;
var onStateChangeCallback; var onStateChangeCallback;
var onLovelaceUpdatedCallback;
IOWebSocketChannel _socket; IOWebSocketChannel _socket;
@ -38,9 +38,8 @@ class ConnectionManager {
Completer completer = Completer(); Completer completer = Completer();
bool stopInit = false; bool stopInit = false;
if (loadSettings) { if (loadSettings) {
Logger.e("Loading settings..."); Logger.d("Loading settings...");
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
useLovelace = prefs.getBool('use-lovelace') ?? true;
_domain = prefs.getString('hassio-domain'); _domain = prefs.getString('hassio-domain');
_port = prefs.getString('hassio-port'); _port = prefs.getString('hassio-port');
webhookId = prefs.getString('app-webhook-id'); webhookId = prefs.getString('app-webhook-id');
@ -59,9 +58,9 @@ class ConnectionManager {
_token = await storage.read(key: "hacl_llt"); _token = await storage.read(key: "hacl_llt");
Logger.e("Long-lived token read successful"); Logger.e("Long-lived token read successful");
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent( oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'http://ha-client.homemade.systems')}&redirect_uri=${Uri 'https://ha-client.app')}&redirect_uri=${Uri
.encodeComponent( .encodeComponent(
'haclient://auth')}"; 'https://ha-client.app/service/auth_callback.html')}";
settingsLoaded = true; settingsLoaded = true;
} catch (e) { } catch (e) {
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()])); completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
@ -98,16 +97,23 @@ class ConnectionManager {
void _doConnect({Completer completer, bool forceReconnect}) { void _doConnect({Completer completer, bool forceReconnect}) {
if (forceReconnect || !isConnected) { if (forceReconnect || !isConnected) {
_connect().timeout(connectTimeout, onTimeout: () { _disconnect().then((_){
_disconnect().then((_) { _connect().timeout(connectTimeout).then((_) {
if (completer != null && !completer.isCompleted) { completer?.complete();
completer.completeError(HAError("Connection timeout")); }).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()}"));
}
});
}); });
}).then((_) {
completer?.complete();
}).catchError((e) {
completer?.completeError(e);
}); });
} else { } else {
completer?.complete(); completer?.complete();
@ -124,40 +130,54 @@ class ConnectionManager {
connecting = Completer(); connecting = Completer();
_disconnect().then((_) { _disconnect().then((_) {
Logger.d("Socket connecting..."); Logger.d("Socket connecting...");
_socket = IOWebSocketChannel.connect( try {
_socket = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15)); _webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
_socketSubscription = _socket.stream.listen( _socketSubscription = _socket.stream.listen(
(message) { (message) {
isConnected = true; isConnected = true;
var data = json.decode(message); var data = json.decode(message);
if (data["type"] == "auth_required") { if (data["type"] == "auth_required") {
Logger.d("[Received] <== ${data.toString()}"); Logger.d("[Received] <== ${data.toString()}");
_authenticate().then((_) { _authenticate().then((_) {
Logger.d('Authentication complete'); Logger.d('Authentication complete');
connecting.complete(); connecting.complete();
}).catchError((e) { }).catchError((e) {
if (!connecting.isCompleted) connecting.completeError(e); if (!connecting.isCompleted) connecting.completeError(e);
}); });
} else if (data["type"] == "auth_ok") { } else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}"); Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.complete(); Logger.d("[Connection] Subscribing to events");
_messageResolver.remove("auth"); sendSocketMessage(
if (_token != null) { type: "subscribe_events",
if (!connecting.isCompleted) connecting.complete(); 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()]));
_messageResolver.remove("auth");
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
} else {
_handleMessage(data);
} }
} else if (data["type"] == "auth_invalid") { },
Logger.d("[Received] <== ${data.toString()}"); cancelOnError: true,
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()])); onDone: () => _handleSocketClose(connecting),
_messageResolver.remove("auth"); onError: (e) => _handleSocketError(e, connecting)
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")])); );
} else { } catch(exeption) {
_handleMessage(data); connecting.completeError(HAError("${exeption.toString()}"));
} }
},
cancelOnError: true,
onDone: () => _handleSocketClose(connecting),
onError: (e) => _handleSocketError(e, connecting)
);
}); });
return connecting.future; return connecting.future;
} }
@ -189,13 +209,24 @@ class ConnectionManager {
//Logger.d("[Received] <== Request id ${data['id']} was successful"); //Logger.d("[Received] <== Request id ${data['id']} was successful");
_messageResolver["${data["id"]}"]?.complete(data["result"]); _messageResolver["${data["id"]}"]?.complete(data["result"]);
} else if (data["id"] != null) { } else if (data["id"] != null) {
//Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}"); Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
_messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}"); _messageResolver["${data["id"]}"]?.completeError("${data["error"]["code"]}");
} }
_messageResolver.remove("${data["id"]}"); _messageResolver.remove("${data["id"]}");
} else if (data["type"] == "event") { } else if (data["type"] == "event") {
if (data["event"] != null) {
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")) { if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
//Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}"); Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
onStateChangeCallback(data["event"]["data"]); onStateChangeCallback(data["event"]["data"]);
} else if (data["event"] != null) { } else if (data["event"] != null) {
Logger.w("Unhandled event type: ${data["event"]["event_type"]}"); Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
@ -209,38 +240,24 @@ class ConnectionManager {
void _handleSocketClose(Completer connectionCompleter) { void _handleSocketClose(Completer connectionCompleter) {
Logger.d("Socket disconnected."); Logger.d("Socket disconnected.");
if (!connectionCompleter.isCompleted) { _disconnect().then((_) {
isConnected = false; if (!connectionCompleter.isCompleted) {
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()])); isConnected = false;
} else { connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
_disconnect().then((_) { }
Timer(Duration(seconds: 5), () { eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
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) { void _handleSocketError(e, Completer connectionCompleter) {
Logger.e("Socket stream Error: $e"); Logger.e("Socket stream Error: $e");
if (!connectionCompleter.isCompleted) { _disconnect().then((_) {
isConnected = false; if (!connectionCompleter.isCompleted) {
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant")); isConnected = false;
} else { connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
_disconnect().then((_) { }
Timer(Duration(seconds: 5), () { eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
Logger.d("Trying to reconnect..."); });
_connect().catchError((e) {
isConnected = false;
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
});
});
});
}
} }
Future _authenticate() { Future _authenticate() {
@ -329,13 +346,13 @@ class ConnectionManager {
_messageResolver[callbackName] = _completer; _messageResolver[callbackName] = _completer;
String rawMessage = json.encode(dataObject); String rawMessage = json.encode(dataObject);
if (!isConnected) { if (!isConnected) {
_connect().timeout(connectTimeout, onTimeout: (){ _connect().timeout(connectTimeout).then((_) {
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
}).then((_) {
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}"); Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
_socket.sink.add(rawMessage); _socket.sink.add(rawMessage);
}).catchError((e) { }).catchError((e) {
_completer.completeError(e); if (!_completer.isCompleted) {
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
}
}); });
} else { } else {
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}"); Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
@ -348,25 +365,27 @@ class ConnectionManager {
_currentMessageId += 1; _currentMessageId += 1;
} }
Future callService({String domain, String service, String entityId, Map additionalServiceData}) { Future callService({@required String domain, @required String service, entityId, Map data}) {
eventBus.fire(NotifyServiceCallEvent(domain, service, entityId));
Logger.d("Service call: $domain.$service, $entityId, $data");
Completer completer = Completer(); Completer completer = Completer();
Map serviceData = {}; Map serviceData = {};
if (entityId != null) { if (entityId != null) {
serviceData["entity_id"] = entityId; serviceData["entity_id"] = entityId;
} }
if (additionalServiceData != null && additionalServiceData.isNotEmpty) { if (data != null && data.isNotEmpty) {
serviceData.addAll(additionalServiceData); serviceData.addAll(data);
} }
if (serviceData.isNotEmpty) if (serviceData.isNotEmpty)
sendHTTPPost( sendHTTPPost(
endPoint: "/api/services/$domain/$service", endPoint: "/api/services/$domain/$service",
data: json.encode(serviceData) data: json.encode(serviceData)
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}"))); ).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}); //return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
else else
sendHTTPPost( sendHTTPPost(
endPoint: "/api/services/$domain/$service" endPoint: "/api/services/$domain/$service"
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}")));; ).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString())));
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service}); //return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
return completer.future; return completer.future;
} }
@ -407,11 +426,12 @@ class ConnectionManager {
headers: headers, headers: headers,
body: data body: data
).then((response) { ).then((response) {
Logger.d("[Received] <== HTTP ${response.statusCode}");
if (response.statusCode >= 200 && response.statusCode < 300 ) { if (response.statusCode >= 200 && response.statusCode < 300 ) {
Logger.d("[Received] <== HTTP ${response.statusCode}");
completer.complete(response.body); completer.complete(response.body);
} else { } else {
completer.completeError({"code": response.statusCode, "message": "${response.body}"}); Logger.d("[Received] <== HTTP ${response.statusCode}: ${response.body}");
completer.completeError(response);
} }
}).catchError((e) { }).catchError((e) {
completer.completeError(e); completer.completeError(e);

View File

@ -14,7 +14,7 @@ class LocationManager {
} }
final int defaultUpdateIntervalMinutes = 20; final int defaultUpdateIntervalMinutes = 20;
final String backgroundTaskId = "haclocationtask4352"; final String backgroundTaskId = "haclocationtask0";
final String backgroundTaskTag = "haclocation"; final String backgroundTaskTag = "haclocation";
Duration _updateInterval; Duration _updateInterval;
bool _isRunning; bool _isRunning;
@ -57,120 +57,181 @@ class LocationManager {
} }
_startLocationService() async { _startLocationService() async {
Logger.d("Scheduling location update for every ${_updateInterval
.inMinutes} minutes...");
String webhookId = ConnectionManager().webhookId; String webhookId = ConnectionManager().webhookId;
String httpWebHost = ConnectionManager().httpWebHost; String httpWebHost = ConnectionManager().httpWebHost;
if (webhookId != null && webhookId.isNotEmpty) { if (webhookId != null && webhookId.isNotEmpty) {
await workManager.Workmanager.registerPeriodicTask( Duration interval;
backgroundTaskId, int delayFactor;
"haClientLocationTracking", int taskCount;
tag: backgroundTaskTag, Logger.d("Starting location update for every ${_updateInterval
inputData: { .inMinutes} minutes...");
"webhookId": webhookId, if (_updateInterval.inMinutes == 10) {
"httpWebHost": httpWebHost interval = Duration(minutes: 20);
}, taskCount = 2;
frequency: _updateInterval, delayFactor = 10;
existingWorkPolicy: workManager.ExistingWorkPolicy.keep, } else if (_updateInterval.inMinutes == 5) {
backoffPolicy: workManager.BackoffPolicy.linear, interval = Duration(minutes: 15);
backoffPolicyDelay: _updateInterval, taskCount = 3;
constraints: workManager.Constraints( delayFactor = 5;
networkType: workManager.NetworkType.connected } 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 { _stopLocationService() async {
Logger.d("Canceling previous schedule if any..."); Logger.d("Canceling previous schedule if any...");
await workManager.Workmanager.cancelByTag(backgroundTaskTag); await workManager.Workmanager.cancelAll();
} }
updateDeviceLocation() async { updateDeviceLocation() async {
if (ConnectionManager().webhookId != null && Logger.d("[Foreground location] Started");
ConnectionManager().webhookId.isNotEmpty) { Geolocator geolocator = Geolocator();
String url = "${ConnectionManager() var battery = Battery();
.httpWebHost}/api/webhook/${ConnectionManager().webhookId}"; String webhookId = ConnectionManager().webhookId;
Map<String, String> headers = {}; String httpWebHost = ConnectionManager().httpWebHost;
Logger.d("[Location] Getting device location..."); if (webhookId != null && webhookId.isNotEmpty) {
Position location = await Geolocator().getCurrentPosition( Logger.d("[Foreground location] Getting battery level...");
desiredAccuracy: LocationAccuracy.medium); int batteryLevel = await battery.batteryLevel;
Logger.d("[Location] Got location: ${location.latitude} ${location Logger.d("[Foreground location] Getting device location...");
.longitude}. Sending home..."); Position position = await geolocator.getCurrentPosition(
int battery = await Battery().batteryLevel; desiredAccuracy: LocationAccuracy.high,
var data = { locationPermissionLevel: GeolocationPermission.locationAlways
"type": "update_location", );
"data": { if (position != null) {
"gps": [location.latitude, location.longitude], Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})");
"gps_accuracy": location.accuracy, String url = "$httpWebHost/api/webhook/$webhookId";
"battery": battery Map data = {
} "type": "update_location",
}; "data": {
headers["Content-Type"] = "application/json"; "gps": [position.latitude, position.longitude],
await http.post( "gps_accuracy": position.accuracy,
"battery": batteryLevel ?? 100
}
};
Logger.d("[Foreground location] Sending data home...");
var response = await http.post(
url, url,
headers: headers, headers: {"Content-Type": "application/json"},
body: json.encode(data) body: json.encode(data)
); );
Logger.d("[Location] ...done."); Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
} else {
Logger.d("[Foreground location] No location. Aborting.");
}
} }
} }
} }
void updateDeviceLocationIsolate() { void updateDeviceLocationIsolate() {
workManager.Workmanager.executeTask((backgroundTask, data) { workManager.Workmanager.executeTask((backgroundTask, data) async {
//print("[Background $backgroundTask] Started"); //print("[Background $backgroundTask] Started");
Geolocator geolocator = Geolocator();
var battery = Battery(); var battery = Battery();
int batteryLevel = 100;
String webhookId = data["webhookId"]; String webhookId = data["webhookId"];
String httpWebHost = data["httpWebHost"]; 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) { if (webhookId != null && webhookId.isNotEmpty) {
//print("[Background $backgroundTask] hour=$battery"); String url = "$httpWebHost/api/webhook/$webhookId";
String url = "$httpWebHost/api/webhook/$webhookId"; Map<String, String> headers = {};
Map<String, String> headers = {}; headers["Content-Type"] = "application/json";
headers["Content-Type"] = "application/json"; Map data = {
Map data = { "type": "update_location",
"type": "update_location", "data": {
"data": { "gps": [],
"gps": [], "gps_accuracy": 0,
"gps_accuracy": 0, "battery": 100
"battery": batteryLevel }
};
//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 {
//print("[Background $backgroundTask] Getting battery level..."); logData += ' || Location: error, location is null';
battery.batteryLevel.then((val) => data["data"]["battery"] = val).whenComplete((){ }*/
//print("[Background $backgroundTask] Getting device location..."); } catch (e) {
Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.medium).then((location) { //print("[Background $backgroundTask] Location error: $e");
//print("[Background $backgroundTask] Got location: ${location.latitude} ${location.longitude}"); //logData += ' || Location: error, $e';
if (location != null) { }
data["data"]["gps"] = [location.latitude, location.longitude]; }/* else {
data["data"]["gps_accuracy"] = location.accuracy; logData += 'Not configured';
//print("[Background $backgroundTask] Sending data home..."); }*/
http.post( //print("[Background $backgroundTask] Writing log data...");
url, /*try {
headers: headers, var fileMode;
body: json.encode(data) if (logFile.existsSync() && logFile.lengthSync() < 5000000) {
); fileMode = FileMode.append;
} } else {
}).catchError((e) { fileMode = FileMode.write;
//print("[Background $backgroundTask] Error getting current location: ${e.toString()}. Trying last known..."); }
Geolocator().getLastKnownPosition(desiredAccuracy: LocationAccuracy.medium).then((location){ await logFile.writeAsString('$logData\n', mode: fileMode);
//print("[Background $backgroundTask] Got last known location: ${location.latitude} ${location.longitude}"); } catch (e) {
if (location != null) { print("[Background $backgroundTask] Error writing log: $e");
data["data"]["gps"] = [location.latitude, location.longitude];
data["data"]["gps_accuracy"] = location.accuracy;
//print("[Background $backgroundTask] Sending data home...");
http.post(
url,
headers: headers,
body: json.encode(data)
);
}
});
});
});
} }
return Future.value(true); print("[Background $backgroundTask] Finished.");*/
return true;
}); });
} }

View File

@ -45,7 +45,7 @@ class MobileAppIntegrationManager {
positiveText: "Restart now", positiveText: "Restart now",
negativeText: "Later", negativeText: "Later",
onPositive: () { onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "restart", entityId: null); ConnectionManager().callService(domain: "homeassistant", service: "restart");
}, },
)); ));
}); });
@ -81,7 +81,7 @@ class MobileAppIntegrationManager {
} }
completer.complete(); completer.complete();
}).catchError((e) { }).catchError((e) {
if (e['code'] != null && e['code'] == 410) { if (e is http.Response && e.statusCode == 410) {
Logger.e("MobileApp integration was removed"); Logger.e("MobileApp integration was removed");
_askToRegisterApp(); _askToRegisterApp();
} else { } else {

View File

@ -9,12 +9,12 @@ class StartupUserMessagesManager {
return _instance; return _instance;
} }
StartupUserMessagesManager._internal() {} StartupUserMessagesManager._internal();
bool _supportAppDevelopmentMessageShown; bool _supportAppDevelopmentMessageShown;
bool _whatsNewMessageShown; bool _whatsNewMessageShown;
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3"; static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
static final _whatsNewMessageKey = "user-message-shown-whats-new-706"; static final _whatsNewMessageKey = "user-message-shown-whats-new-884";
void checkMessagesToShow() async { void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();

View 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,
),
);
}
}

View File

@ -29,6 +29,9 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
setState(() { setState(() {
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false; _locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
_locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes; _locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
if (_locationInterval % 5 != 0) {
_locationInterval = 5 * (_locationInterval ~/ 5);
}
}); });
}); });
} }
@ -36,15 +39,15 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
void incLocationInterval() { void incLocationInterval() {
if (_locationInterval < 720) { if (_locationInterval < 720) {
setState(() { setState(() {
_locationInterval = _locationInterval + 1; _locationInterval = _locationInterval + 5;
}); });
} }
} }
void decLocationInterval() { void decLocationInterval() {
if (_locationInterval > 1) { if (_locationInterval > 5) {
setState(() { setState(() {
_locationInterval = _locationInterval - 1; _locationInterval = _locationInterval - 5;
}); });
} }
} }
@ -56,7 +59,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
positiveText: "Sure. Make it so", positiveText: "Sure. Make it so",
negativeText: "What?? No!", negativeText: "What?? No!",
onPositive: () { onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "restart", entityId: null); ConnectionManager().callService(domain: "homeassistant", service: "restart");
}, },
)); ));
} }
@ -68,7 +71,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
positiveText: "Sure. Make it so", positiveText: "Sure. Make it so",
negativeText: "What?? No!", negativeText: "What?? No!",
onPositive: () { onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "stop", entityId: null); ConnectionManager().callService(domain: "homeassistant", service: "stop");
}, },
)); ));
} }
@ -115,7 +118,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
Text("Location tracking", style: TextStyle(fontSize: Sizes.largeFontSize-2)), Text("Location tracking", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
Container(height: Sizes.rowPadding,), Container(height: Sizes.rowPadding,),
InkWell( InkWell(
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#location-tracking"), onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#location-tracking"),
child: Text( child: Text(
"Please read documentation!", "Please read documentation!",
style: TextStyle( style: TextStyle(

View File

@ -1,4 +1,4 @@
part of '../main.dart'; part of '../../main.dart';
class MainPage extends StatefulWidget { class MainPage extends StatefulWidget {
MainPage({Key key, this.title}) : super(key: key); MainPage({Key key, this.title}) : super(key: key);
@ -9,10 +9,10 @@ class MainPage extends StatefulWidget {
_MainPageState createState() => new _MainPageState(); _MainPageState createState() => new _MainPageState();
} }
class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin { class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
StreamSubscription<List<PurchaseDetails>> _subscription;
StreamSubscription _stateSubscription; StreamSubscription _stateSubscription;
StreamSubscription _lovelaceSubscription;
StreamSubscription _settingsSubscription; StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription; StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription; StreamSubscription _showEntityPageSubscription;
@ -25,18 +25,11 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
int _previousViewCount; int _previousViewCount;
bool _showLoginButton = false; bool _showLoginButton = false;
bool _preventAppRefresh = false; bool _preventAppRefresh = false;
String _savedSharedText; Entity _entityToShow;
String _entityToShow;
@override @override
void initState() { void initState() {
final Stream purchaseUpdates =
InAppPurchaseConnection.instance.purchaseUpdatedStream;
_subscription = purchaseUpdates.listen((purchases) {
_handlePurchaseUpdates(purchases);
});
super.initState(); super.initState();
enableShareReceiving();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_firebaseMessaging.configure( _firebaseMessaging.configure(
@ -77,12 +70,6 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
_fullLoad(); _fullLoad();
} }
@override void receiveShare(Share shared) {
if (shared.mimeType == ShareType.TYPE_PLAIN_TEXT) {
_savedSharedText = shared.text;
}
}
Future onSelectNotification(String payload) async { Future onSelectNotification(String payload) async {
if (payload != null) { if (payload != null) {
Logger.d('Notification clicked: ' + payload); Logger.d('Notification clicked: ' + payload);
@ -104,43 +91,42 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
); );
} }
void _fullLoad() async { void _fullLoad() {
_showInfoBottomBar(progress: true,); _showInfoBottomBar(progress: true,);
_subscribe().then((_) { _subscribe().then((_) {
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){ ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
_fetchData(); SharedPreferences.getInstance().then((prefs) {
LocationManager(); HomeAssistant().lovelaceDashboardUrl = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
StartupUserMessagesManager().checkMessagesToShow(); _fetchData(useCache: true);
LocationManager();
StartupUserMessagesManager().checkMessagesToShow();
});
}, onError: (e) { }, onError: (e) {
_setErrorState(e); _setErrorState(e);
}); });
}); });
} }
void _quickLoad() { void _quickLoad({bool uiOnly: false}) {
_hideBottomBar(); _hideBottomBar();
_showInfoBottomBar(progress: true,); _showInfoBottomBar(progress: true,);
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){ ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
_fetchData(); _fetchData(useCache: false, uiOnly: uiOnly);
//StartupUserMessagesManager().checkMessagesToShow();
}, onError: (e) { }, onError: (e) {
_setErrorState(e); _setErrorState(e);
}); });
} }
_fetchData() async { _fetchData({useCache: false, uiOnly: false}) async {
if (_savedSharedText != null && !HomeAssistant().isNoEntities) { if (useCache && !uiOnly) {
Logger.d("Got shared text: $_savedSharedText"); HomeAssistant().fetchDataFromCache().then((_) {
Navigator.pushNamed(context, "/play-media", arguments: {"url": _savedSharedText}); setState((){});
_savedSharedText = null; });
} }
await HomeAssistant().fetchData().then((_) { await HomeAssistant().fetchData(uiOnly).then((_) {
_hideBottomBar(); _hideBottomBar();
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0; if (_entityToShow != null) {
if (_previousViewCount != currentViewCount) { _entityToShow = HomeAssistant().entities.get(_entityToShow.entityId);
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
_viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount;
} }
}).catchError((e) { }).catchError((e) {
if (e is HAError) { if (e is HAError) {
@ -157,40 +143,32 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
Logger.d("$state"); Logger.d("$state");
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) { if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
_quickLoad(); _quickLoad();
} } else if (state == AppLifecycleState.paused && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
} HomeAssistant().saveCache();
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");
} }
} }
Future _subscribe() { Future _subscribe() {
Completer completer = Completer(); Completer completer = Completer();
if (_stateSubscription == null) { if (_stateSubscription == null) {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) { if (event.needToRebuildUI) {
Logger.d("New entity. Need to rebuild UI"); Logger.d("Need to rebuild UI");
_quickLoad(); _quickLoad();
} else { } else {
setState(() {}); setState(() {});
} }
}); });
} }
if (_lovelaceSubscription == null) {
_lovelaceSubscription = eventBus.on<LovelaceChangedEvent>().listen((event) {
_quickLoad();
});
}
if (_reloadUISubscription == null) { if (_reloadUISubscription == null) {
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){ _reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
_quickLoad(); _quickLoad(uiOnly: true);
}); });
} }
if (_showPopupDialogSubscription == null) { if (_showPopupDialogSubscription == null) {
@ -218,9 +196,8 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
} }
if (_serviceCallSubscription == null) { if (_serviceCallSubscription == null) {
_serviceCallSubscription = _serviceCallSubscription =
eventBus.on<ServiceCallEvent>().listen((event) { eventBus.on<NotifyServiceCallEvent>().listen((event) {
_callService(event.domain, event.service, event.entityId, _notifyServiceCalled(event.domain, event.service, event.entityId);
event.additionalParams);
}); });
} }
@ -253,6 +230,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
_showOAuth(); _showOAuth();
} else { } else {
_preventAppRefresh = false; _preventAppRefresh = false;
Navigator.of(context).pop();
} }
}); });
} }
@ -266,9 +244,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
void _showOAuth() { void _showOAuth() {
_preventAppRefresh = true; _preventAppRefresh = true;
Launcher.launchURLInCustomTab( Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
url: ConnectionManager().oauthUrl
);
} }
_setErrorState(HAError e) { _setErrorState(HAError e) {
@ -318,27 +294,28 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
); );
} }
//TODO remove this shit.... maybe void _notifyServiceCalled(String domain, String service, entityId) {
void _callService(String domain, String service, String entityId, Map additionalParams) {
_showInfoBottomBar( _showInfoBottomBar(
message: "Calling $domain.$service", 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) { void _showEntityPage(String entityId) {
setState(() { setState(() {
_entityToShow = entityId; _entityToShow = HomeAssistant().entities?.get(entityId);
if (_entityToShow != null) {
_mainScrollController?.jumpTo(0);
}
}); });
if (_entityToShow!= null && MediaQuery.of(context).size.width < Sizes.tabletMinWidth) { /*if (_entityToShow!= null && MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => EntityViewPage(entityId: entityId), builder: (context) => EntityViewPage(entityId: entityId),
) )
); );
} }*/
} }
void _showPage(String path, bool goBackFirst) { void _showPage(String path, bool goBackFirst) {
@ -368,12 +345,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
menuItems.add( menuItems.add(
UserAccountsDrawerHeader( UserAccountsDrawerHeader(
accountName: Text(HomeAssistant().userName), accountName: Text(HomeAssistant().userName),
accountEmail: Text(ConnectionManager().displayHostname ?? "Not configured"), accountEmail: Text(HomeAssistant().locationName ?? ""),
onDetailsPressed: () {
Launcher.launchURLInCustomTab(
url: "${ConnectionManager().httpWebHost}/profile?external_auth=1"
);
},
currentAccountPicture: CircleAvatar( currentAccountPicture: CircleAvatar(
child: Text( child: Text(
HomeAssistant().userAvatarText, HomeAssistant().userAvatarText,
@ -395,7 +367,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
children: <Widget>[ children: <Widget>[
Text("${panel.title}"), Text("${panel.title}"),
Container(width: 4.0,), Container(width: 4.0,),
panel.isWebView ? Text("WEB", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,) panel.isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
], ],
), ),
onTap: () { onTap: () {
@ -459,26 +431,33 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
title: Text("Help"), title: Text("Help"),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Launcher.launchURL("http://ha-client.homemade.systems/docs"); Launcher.launchURL("http://ha-client.app/docs");
}, },
), ),
new ListTile( new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")), leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:forum")),
title: Text("Join Discord channel"), title: Text("Contacts/Discussion"),
onTap: () { onTap: () {
Navigator.of(context).pop(); 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( new AboutListTile(
aboutBoxChildren: <Widget>[ aboutBoxChildren: <Widget>[
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Launcher.launchURL("http://ha-client.homemade.systems/"); Launcher.launchURL("http://ha-client.app/");
}, },
child: Text( child: Text(
"ha-client.homemade.systems", "ha-client.app",
style: TextStyle( style: TextStyle(
color: Colors.blue, color: Colors.blue,
decoration: TextDecoration.underline decoration: TextDecoration.underline
@ -491,7 +470,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.of(context).pop(); 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( child: Text(
"Terms and Conditions", "Terms and Conditions",
@ -507,7 +486,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.of(context).pop(); 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( child: Text(
"Privacy Policy", "Privacy Policy",
@ -636,11 +615,19 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
} }
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final ScrollController _mainScrollController = ScrollController();
Widget _buildScaffoldBody(bool empty) { Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> serviceMenuItems = []; List<PopupMenuItem<String>> serviceMenuItems = [];
List<PopupMenuItem<String>> mediaMenuItems = []; List<PopupMenuItem<String>> mediaMenuItems = [];
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>( serviceMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"), child: new Text("Reload"),
value: "reload", value: "reload",
@ -656,7 +643,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
Widget mediaMenuIcon; Widget mediaMenuIcon;
int playersCount = 0; int playersCount = 0;
if (!empty && !HomeAssistant().entities.isEmpty) { if (!empty && !HomeAssistant().entities.isEmpty) {
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(domains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]); List<Entity> activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
playersCount = activePlayers.length; playersCount = activePlayers.length;
mediaMenuItems.addAll( mediaMenuItems.addAll(
activePlayers.map((entity) => PopupMenuItem<String>( activePlayers.map((entity) => PopupMenuItem<String>(
@ -730,12 +717,11 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
} }
} else { } else {
if (_entityToShow != null && MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) { if (_entityToShow != null && MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
Entity entity = HomeAssistant().entities.get(_entityToShow);
mainScrollBody = Flex( mainScrollBody = Flex(
direction: Axis.horizontal, direction: Axis.horizontal,
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: HomeAssistant().buildViews(context, _viewsTabController), child: HomeAssistant().ui.build(context, _viewsTabController),
), ),
Container( Container(
width: Sizes.mainPageScreenSeparatorWidth, width: Sizes.mainPageScreenSeparatorWidth,
@ -743,13 +729,14 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
), ),
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints.tightFor(width: Sizes.entityPageMaxWidth), constraints: BoxConstraints.tightFor(width: Sizes.entityPageMaxWidth),
child: EntityPageLayout(entity: entity, showClose: true,), child: EntityPageLayout(entity: _entityToShow, showClose: true,),
) )
], ],
); );
} else if (_entityToShow != null) {
mainScrollBody = EntityPageLayout(entity: _entityToShow, showClose: true,);
} else { } else {
_entityToShow = null; mainScrollBody = HomeAssistant().ui.build(context, _viewsTabController);
mainScrollBody = HomeAssistant().buildViews(context, _viewsTabController);
} }
} }
@ -787,7 +774,9 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
context: context, context: context,
items: serviceMenuItems items: serviceMenuItems
).then((String val) { ).then((String val) {
HomeAssistant().lovelaceDashboardUrl = HomeAssistant.DEFAULT_DASHBOARD;
if (val == "reload") { if (val == "reload") {
_quickLoad(); _quickLoad();
} else if (val == "logout") { } else if (val == "logout") {
HomeAssistant().logout().then((_) { HomeAssistant().logout().then((_) {
@ -804,7 +793,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
_scaffoldKey.currentState.openDrawer(); _scaffoldKey.currentState.openDrawer();
}, },
), ),
bottom: empty ? null : TabBar( bottom: (empty || _entityToShow != null) ? null : TabBar(
controller: _viewsTabController, controller: _viewsTabController,
tabs: buildUIViewTabs(), tabs: buildUIViewTabs(),
isScrollable: true, isScrollable: true,
@ -813,7 +802,8 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
]; ];
}, },
body: mainScrollBody body: mainScrollBody,
controller: _mainScrollController,
); );
} }
@ -869,31 +859,43 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
); );
} }
} }
// This method is rerun every time setState is called.
if (HomeAssistant().isNoViews) { if (HomeAssistant().isNoViews) {
return Scaffold( return Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
primary: false, primary: false,
drawer: _buildAppDrawer(), drawer: _buildAppDrawer(),
bottomNavigationBar: bottomBar, bottomNavigationBar: bottomBar,
body: _buildScaffoldBody(true) body: _buildScaffoldBody(true)
); );
} else { } else {
return Scaffold( return WillPopScope(
key: _scaffoldKey, child: Scaffold(
drawer: _buildAppDrawer(), key: _scaffoldKey,
primary: false, drawer: _buildAppDrawer(),
bottomNavigationBar: bottomBar, primary: false,
body: _buildScaffoldBody(false) bottomNavigationBar: bottomBar,
); body: _buildScaffoldBody(false)
} ),
onWillPop: () {
if (_entityToShow != null) {
eventBus.fire(ShowEntityPageEvent());
return Future.value(false);
} else {
return Future.value(true);
}
},
);
}
} }
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
//final flutterWebviewPlugin = new FlutterWebviewPlugin();
//flutterWebviewPlugin.dispose();
_viewsTabController?.dispose(); _viewsTabController?.dispose();
_stateSubscription?.cancel(); _stateSubscription?.cancel();
_lovelaceSubscription?.cancel();
_settingsSubscription?.cancel(); _settingsSubscription?.cancel();
_serviceCallSubscription?.cancel(); _serviceCallSubscription?.cancel();
_showPopupDialogSubscription?.cancel(); _showPopupDialogSubscription?.cancel();
@ -901,7 +903,6 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
_showEntityPageSubscription?.cancel(); _showEntityPageSubscription?.cancel();
_showErrorSubscription?.cancel(); _showErrorSubscription?.cancel();
_startAuthSubscription?.cancel(); _startAuthSubscription?.cancel();
_subscription?.cancel();
_showPageSubscription?.cancel(); _showPageSubscription?.cancel();
_reloadUISubscription?.cancel(); _reloadUISubscription?.cancel();
//TODO disconnect //TODO disconnect

View File

@ -57,9 +57,9 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
_loaded = false; _loaded = false;
}); });
} else { } else {
_isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor"); _isMediaExtractorExist = HomeAssistant().isServiceExist("media_extractor");
//_useMediaExtractor = _isMediaExtractorExist; //_useMediaExtractor = _isMediaExtractorExist;
_players = HomeAssistant().entities.getByDomains(domains: ["media_player"]); _players = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"]);
setState(() { setState(() {
if (_players.isNotEmpty) { if (_players.isNotEmpty) {
_loaded = true; _loaded = true;
@ -90,16 +90,20 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
Navigator.pop(context); Navigator.pop(context);
ConnectionManager().callService( ConnectionManager().callService(
domain: serviceDomain, domain: serviceDomain,
entityId: entity.entityId,
service: "play_media", service: "play_media",
additionalServiceData: { entityId: entity.entityId,
"media_content_id": _mediaUrl, data: {
"media_content_type": _contentType "media_content_id": _mediaUrl,
} "media_content_type": _contentType
}
); );
HomeAssistant().sendToPlayerId = entity.entityId; HomeAssistant().sendToPlayerId = entity.entityId;
if (HomeAssistant().sendFromPlayerId != null && HomeAssistant().sendFromPlayerId != HomeAssistant().sendToPlayerId) { if (HomeAssistant().sendFromPlayerId != null && HomeAssistant().sendFromPlayerId != HomeAssistant().sendToPlayerId) {
eventBus.fire(ServiceCallEvent(HomeAssistant().sendFromPlayerId.split(".")[0], "turn_off", HomeAssistant().sendFromPlayerId, null)); ConnectionManager().callService(
domain: HomeAssistant().sendFromPlayerId.split(".")[0],
service: "turn_off",
entityId: HomeAssistant().sendFromPlayerId
);
HomeAssistant().sendFromPlayerId = null; HomeAssistant().sendFromPlayerId = null;
} }
eventBus.fire(ShowEntityPageEvent(entity: entity)); eventBus.fire(ShowEntityPageEvent(entity: entity));
@ -241,5 +245,5 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
_refreshDataSubscription?.cancel(); _refreshDataSubscription?.cancel();
super.dispose(); super.dispose();
} }
} }

View File

@ -75,10 +75,16 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
_saveSettings() async { _saveSettings() async {
_newHassioDomain = _newHassioDomain.trim(); _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("//")[1];
} }
_newHassioDomain = _newHassioDomain.split("/")[0]; _newHassioDomain = _newHassioDomain.split("/")[0];
if (_newHassioDomain.contains(":")) {
List<String> domainAndPort = _newHassioDomain.split(":");
_newHassioDomain = domainAndPort[0];
_newHassioPort = domainAndPort[1];
}
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage(); final storage = new FlutterSecureStorage();
if (_newLongLivedToken.isNotEmpty) { if (_newLongLivedToken.isNotEmpty) {

View File

@ -24,7 +24,7 @@ class _WhatsNewPageState extends State<WhatsNewPage> {
error = ""; error = "";
}); });
http.Response response; http.Response response;
response = await http.get("http://ha-client.homemade.systems/service/whats_new_$appVersionNumber.md"); response = await http.get("http://ha-client.app/service/whats_new_0.8.md");
if (response.statusCode == 200) { if (response.statusCode == 200) {
setState(() { setState(() {
data = response.body; data = response.body;

90
lib/pages/zha_page.dart Normal file
View 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
);
}
}

View File

@ -11,7 +11,7 @@ class Panel {
}; };
final String id; final String id;
final String type; final String componentName;
final String title; final String title;
final String urlPath; final String urlPath;
final Map config; final Map config;
@ -19,34 +19,61 @@ class Panel {
bool isHidden = true; bool isHidden = true;
bool isWebView = false; bool isWebView = false;
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) { Panel({this.id, this.componentName, this.title, this.urlPath, this.icon, this.config}) {
if (icon == null || !icon.startsWith("mdi:")) { if (icon == null || !icon.startsWith("mdi:")) {
icon = Panel.iconsByComponent[type]; icon = Panel.iconsByComponent[componentName];
} }
isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools'); isHidden = (componentName == 'kiosk' || componentName == 'states' || componentName == 'profile' || componentName == 'developer-tools');
isWebView = (type != 'config'); isWebView = (componentName != 'config' && componentName != 'lovelace' && !componentName.startsWith('haclient'));
} }
void handleOpen(BuildContext context) { void handleOpen(BuildContext context) {
if (type == "config") { if (componentName == "config") {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => PanelPage(title: "$title", panel: this), 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 { } else {
Launcher.launchURLInCustomTab(url: "${ConnectionManager().httpWebHost}/$urlPath"); 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() { Widget getWidget() {
switch (type) { switch (componentName) {
case "config": { case "config": {
return ConfigPanelWidget(); return ConfigPanelWidget();
} }
default: { default: {
return Text("Unsupported panel component: $type"); return Text("Unsupported panel component: $componentName");
} }
} }
} }

View File

@ -19,7 +19,7 @@ class LinkToWebConfig extends StatelessWidget {
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)), style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
subtitle: Text("Tap to open web version"), subtitle: Text("Tap to open web version"),
onTap: () { onTap: () {
Launcher.launchURLInCustomTab(url: this.url); Launcher.launchAuthenticatedWebView(context: context, url: this.url, title: this.name);
}, },
) )
], ],

View File

@ -12,6 +12,8 @@ class StateChangedEvent {
}); });
} }
class LovelaceChangedEvent {}
class SettingsChangedEvent { class SettingsChangedEvent {
bool reconnect; bool reconnect;
@ -33,13 +35,12 @@ class StartAuthEvent {
StartAuthEvent(this.oauthUrl, this.showButton); StartAuthEvent(this.oauthUrl, this.showButton);
} }
class ServiceCallEvent { class NotifyServiceCallEvent {
String domain; String domain;
String service; String service;
String entityId; var entityId;
Map<String, dynamic> additionalParams;
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams); NotifyServiceCallEvent(this.domain, this.service, this.entityId);
} }
class ShowPopupDialogEvent { class ShowPopupDialogEvent {

View File

@ -6,8 +6,51 @@ class HomeAssistantUI {
bool get isEmpty => views == null || views.isEmpty; bool get isEmpty => views == null || views.isEmpty;
HomeAssistantUI() { HomeAssistantUI({rawLovelaceConfig}) {
if (rawLovelaceConfig == null) {
rawLovelaceConfig = _generateLovelaceConfig();
}
views = []; views = [];
Logger.d("--Title: ${rawLovelaceConfig["title"]}");
title = rawLovelaceConfig["title"];
int viewCounter = 0;
Logger.d("--Views count: ${rawLovelaceConfig['views'].length}");
rawLovelaceConfig["views"].forEach((rawView){
Logger.d("----view id: ${rawView['id']}");
HAView view = HAView(
count: viewCounter,
rawData: rawView
);
views.add(
view
);
viewCounter += 1;
});
}
Map _generateLovelaceConfig() {
Map result = {};
result['title'] = 'Home';
result['views'] = [
{
'icon': 'mdi:home',
'badges': HomeAssistant().entities.getByDomains(
includeDomains: ['sensor', 'binary_sensor', 'device_tracker', 'person', 'sun']
).map(
(en) => en.entityId
).toList(),
'cards': [{
'type': 'entities',
'entities': HomeAssistant().entities.getByDomains(
excludeDomains: ['sensor','binary_sensor', 'device_tracker', 'person', 'sun']
).map(
(en) => en.entityId
).toList()
}]
}
];
return result;
} }
Widget build(BuildContext context, TabController tabController) { Widget build(BuildContext context, TabController tabController) {

View File

@ -35,4 +35,28 @@ class Launcher {
} }
} }
static void launchAuthenticatedWebView({BuildContext context, String url, String title}) {
if (url.contains("?")) {
url += "&external_auth=1";
} else {
url += "?external_auth=1";
}
final flutterWebViewPlugin = new standaloneWebview.FlutterWebviewPlugin();
flutterWebViewPlugin.onStateChanged.listen((viewState) async {
if (viewState.type == standaloneWebview.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": "$url",
"title": "${title ?? ''}"
}
);
}
} }

View File

@ -23,6 +23,10 @@ class Logger {
return inDebugMode; return inDebugMode;
} }
static void p(data) {
print(data);
}
static void e(String message) { static void e(String message) {
_writeToLog("Error", message); _writeToLog("Error", message);
} }

View File

@ -4,77 +4,171 @@ class HAView {
List<HACard> cards = []; List<HACard> cards = [];
List<Entity> badges = []; List<Entity> badges = [];
Entity linkedEntity; Entity linkedEntity;
final String name; String name;
final String id; String id;
final String iconName; String iconName;
final int count; final int count;
final bool panel; bool isPanel;
HAView({ HAView({@required this.count, @required rawData}) {
this.name, id = "${rawData['id']}";
this.id, name = rawData['title'];
this.count, iconName = rawData['icon'];
this.iconName, isPanel = rawData['panel'] ?? false;
this.panel: false,
List<Entity> childEntities if (rawData['badges'] != null && rawData['badges'] is List) {
}) { rawData['badges'].forEach((entity) {
if (childEntities != null) { if (entity is String) {
_fillView(childEntities); if (HomeAssistant().entities.isExist(entity)) {
} Entity e = HomeAssistant().entities.get(entity);
badges.add(e);
}
} else {
String eId = '${entity['entity']}';
if (HomeAssistant().entities.isExist(eId)) {
Entity e = HomeAssistant().entities.get(eId);
badges.add(e);
}
}
});
}
cards.addAll(_createLovelaceCards(rawData["cards"] ?? []));
} }
void _fillView(List<Entity> childEntities) { List<HACard> _createLovelaceCards(List rawCards) {
List<HACard> autoGeneratedCards = []; List<HACard> result = [];
badges.addAll(childEntities.where((entity){ return entity.isBadge;})); rawCards.forEach((rawCard){
childEntities.where((entity){ return entity.domain == "media_player";}).forEach((e){ try {
HACard card = HACard( //bool isThereCardOptionsInside = rawCard["card"] != null;
name: e.displayName, var rawCardInfo = rawCard["card"] ?? rawCard;
id: e.entityId,
linkedEntityWrapper: EntityWrapper(entity: e),
type: CardType.MEDIA_CONTROL
);
cards.add(card);
});
childEntities.where((e){return (!e.isBadge && e.domain != "media_player");}).forEach((entity) {
if (!entity.isGroup) {
String groupIdToAdd = "${entity.domain}.${entity.domain}$count";
if (autoGeneratedCards.every((HACard card) => card.id != groupIdToAdd )) {
HACard card = HACard(
id: groupIdToAdd,
name: entity.domain,
type: CardType.ENTITIES
);
card.entities.add(EntityWrapper(entity: entity));
autoGeneratedCards.add(card);
} else {
autoGeneratedCards.firstWhere((card) => card.id == groupIdToAdd).entities.add(EntityWrapper(entity: entity));
}
} else {
HACard card = HACard( HACard card = HACard(
name: entity.displayName, id: "card",
id: entity.entityId, name: rawCardInfo["title"] ?? rawCardInfo["name"],
linkedEntityWrapper: EntityWrapper(entity: entity), type: rawCardInfo['type'] ?? CardType.ENTITIES,
type: CardType.ENTITIES columnsCount: rawCardInfo['columns'] ?? 4,
showName: (rawCardInfo['show_name'] ?? rawCard['show_name']) ?? true,
showHeaderToggle: (rawCardInfo['show_header_toggle'] ?? rawCard['show_header_toggle']) ?? true,
showState: (rawCardInfo['show_state'] ?? rawCard['show_state']) ?? true,
showEmpty: (rawCardInfo['show_empty'] ?? rawCard['show_empty']) ?? true,
stateFilter: (rawCard['state_filter'] ?? rawCardInfo['state_filter']) ?? [],
states: rawCardInfo['states'],
conditions: rawCard['conditions'] ?? [],
content: rawCardInfo['content'],
min: rawCardInfo['min'] ?? 0,
max: rawCardInfo['max'] ?? 100,
unit: rawCardInfo['unit'],
severity: rawCardInfo['severity']
); );
card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);})); if (rawCardInfo["cards"] != null) {
entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){ card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
HACard mediaCard = HACard( }
name: entity.displayName, var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
id: entity.entityId, rawEntities?.forEach((rawEntity) {
linkedEntityWrapper: EntityWrapper(entity: entity), if (rawEntity is String) {
type: CardType.MEDIA_CONTROL if (HomeAssistant().entities.isExist(rawEntity)) {
); card.entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
cards.add(mediaCard); } 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 (HomeAssistant().entities.isExist(rawEntity["entity"])) {
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
card.entities.add(
EntityWrapper(
entity: e,
overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
}
}
}); });
cards.add(card); var rawSingleEntity = rawCard["entity"] ?? rawCardInfo["entity"];
if (rawSingleEntity != null) {
var en = rawSingleEntity;
if (en is String) {
if (HomeAssistant().entities.isExist(en)) {
Entity e = HomeAssistant().entities.get(en);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
overrideIcon: rawCardInfo["icon"],
overrideName: rawCardInfo["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
}
} else {
if (HomeAssistant().entities.isExist(en["entity"])) {
Entity e = HomeAssistant().entities.get(en["entity"]);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
overrideIcon: en["icon"],
overrideName: en["name"],
stateFilter: en['state_filter'] ?? [],
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()}");
} }
}); });
cards.addAll(autoGeneratedCards); return result;
} }
Widget buildTab() { Widget buildTab() {
if (linkedEntity == null) { if (linkedEntity == null) {
if (iconName != null) { if (iconName != null && iconName.isNotEmpty) {
return return
Tab( Tab(
icon: icon:

View File

@ -10,22 +10,28 @@ class ViewWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (this.view.panel) { if (this.view.isPanel) {
return FractionallySizedBox( return FractionallySizedBox(
widthFactor: 1, widthFactor: 1,
heightFactor: 1, heightFactor: 1,
child: _buildPanelChild(context), child: _buildPanelChild(context),
); );
} else { } else {
Widget cardsContainer;
if (this.view.cards.isNotEmpty) {
cardsContainer = DynamicMultiColumnLayout(
minColumnWidth: Sizes.minViewColumnWidth,
children: this.view.cards.map((card) => card.build(context)).toList(),
);
} else {
cardsContainer = Container();
}
return ListView( return ListView(
shrinkWrap: true, shrinkWrap: true,
padding: EdgeInsets.all(0), padding: EdgeInsets.all(0),
children: <Widget>[ children: <Widget>[
_buildBadges(context), _buildBadges(context),
DynamicMultiColumnLayout( cardsContainer
minColumnWidth: Sizes.minViewColumnWidth,
children: this.view.cards.map((card) => card.build(context)).toList(),
)
] ]
); );
} }

View File

@ -1,36 +1,39 @@
name: hass_client name: hass_client
description: Home Assistant Android Client description: Home Assistant Android Client
version: 0.7.1+712 version: 0.8.0+886
environment: environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0" sdk: ">=2.2.0 <3.0.0"
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
web_socket_channel: any web_socket_channel: ^1.1.0
shared_preferences: any shared_preferences: ^0.5.6+1
progress_indicators: any progress_indicators: ^0.1.4
event_bus: any path_provider: ^1.6.5
cached_network_image: any event_bus: ^1.1.1
url_launcher: any cached_network_image: ^2.0.0
date_format: any url_launcher: ^5.4.1
date_format: ^1.0.8
charts_flutter: ^0.8.1 charts_flutter: ^0.8.1
flutter_markdown: any flutter_markdown: ^0.3.3
in_app_purchase: ^0.2.1+4 in_app_purchase: ^0.3.0+3
flutter_custom_tabs: ^0.6.0 flutter_custom_tabs: ^0.6.0
firebase_messaging: ^5.1.6 flutter_webview_plugin: ^0.3.10+1
uni_links: ^0.2.0 webview_flutter: ^0.3.19+7
firebase_messaging: ^6.0.9
flutter_secure_storage: ^3.3.1+1 flutter_secure_storage: ^3.3.1+1
device_info: ^0.4.0+3 device_info: ^0.4.1+4
flutter_local_notifications: ^0.8.4 flutter_local_notifications: ^1.1.6
geolocator: ^5.1.4+2 geolocator: ^5.3.1
workmanager: ^0.1.3 workmanager: ^0.2.2
battery: ^0.3.1+1 battery: ^0.3.1+7
share: firebase_crashlytics: ^0.1.3+3
git: video_player: ^0.10.7
url: https://github.com/d-silveira/flutter-share.git
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -49,8 +52,9 @@ flutter:
assets: assets:
- images/hassio-192x192.png - images/hassio-192x192.png
- assets/js/externalAuth.js - assets/js/externalAuth.js
- assets/html/cameraView.html
fonts: fonts:
- family: "Material Design Icons" - family: "Material Design Icons"
fonts: fonts:
- asset: fonts/materialdesignicons-webfont-4.5.95.ttf - asset: fonts/materialdesignicons-webfont-4.5.95.ttf