Compare commits

...

140 Commits

Author SHA1 Message Date
56a333a852 v.0.6.6 2019-09-09 14:25:27 +03:00
c5922368de Add whars new user message 2019-09-09 14:24:54 +03:00
8c2316a51a Resolves #446 Fix conditional crads 2019-09-09 13:36:33 +03:00
e2e6c015de Fix state color for paused media_player 2019-09-09 12:28:32 +03:00
0a6ff4586d Share media url to HA CLient to play on media_player 2019-09-09 12:25:13 +03:00
fc228d85ae Disable wip card 2019-09-08 19:06:41 +03:00
61823cb43b Merge pull request #445 from estevez-dev/feature/light_card_support
WIP #212 Light card support
2019-09-08 19:05:18 +03:00
127e0b8182 WIP #212 Light card support 2019-09-08 19:04:12 +03:00
38c37fa212 Launch camera view in Chrome custom tab 2019-09-07 19:26:00 +03:00
dfaf2a2924 Project structure change 2019-09-07 18:23:04 +03:00
c90c40c046 Resolves #443 Lovelace view as panel support 2019-09-07 17:58:00 +03:00
d2049b726a Resolves #348 Button entity refactoring 2019-09-07 17:27:23 +03:00
6508f109f7 Minor gauge fixes 2019-09-07 17:04:40 +03:00
37e63637a7 Resolves #348 Glance card improvements 2019-09-07 16:46:41 +03:00
6650c5c145 Resolves #208 Gauge card 2019-09-07 15:47:09 +03:00
9160dbf7f2 0.6.5 2019-09-05 15:43:13 +03:00
243fcd7c49 Resolves #430 Trim any leading and trailing whitespace in address, port or token 2019-09-05 00:51:29 +03:00
c114bcfb35 Resolves #426 Autofill port if not set 2019-09-05 00:48:20 +03:00
83defb08f1 Resolves #425 Paste option for text fields 2019-09-05 00:43:45 +03:00
57ebdbbe85 Main page in separate file 2019-09-05 00:25:03 +03:00
c6aceed623 utils separated 2019-09-05 00:17:08 +03:00
ba4c88ec5d Startup user messages fix 2019-09-05 00:09:40 +03:00
ee1685e981 Refix #439 2019-09-04 23:42:19 +03:00
996fbf7bba Login error handling improvements 2019-09-04 23:40:37 +03:00
56cd8963d7 Fix mobile_app error help url 2019-09-04 23:13:11 +03:00
5759aad0cb Remove unused plugin 2019-09-04 23:09:17 +03:00
02717332f7 Revert all rash decisions 2019-09-04 22:46:14 +03:00
8d1b159f56 User error messages 2019-09-04 22:03:52 +03:00
fb335e1100 WIP: user messages 2019-09-04 15:10:25 +03:00
5f0bc83d67 Resolves #439 Fix negative and zero columns issue for glance card 2019-09-03 23:58:28 +03:00
6a8cee2cc2 Login errors handling improvements 2019-09-03 23:44:03 +03:00
0d2f1cf9aa No mobile_app error handling 2019-09-03 23:35:33 +03:00
8efeb3da8a Error messages refactored 2019-09-03 23:25:39 +03:00
620aa3b8d8 Disabling location tracking 2019-09-03 20:13:23 +03:00
ab5bf3b807 WIP #49 Catch http errors inside location isolate 2019-09-02 21:21:38 +03:00
6663bcad72 Resolves #339 Authenticated webview for panels and config sections 2019-09-02 21:08:20 +03:00
113cd29f74 Resolves #431 Add default card type if not specified 2019-09-02 19:36:20 +03:00
f2fdfb0a32 WIP #339 Open unsupported Panels as authenticared webviews 2019-09-02 19:05:49 +03:00
691e48a36b 0.6.5-alpha2 2019-09-01 23:20:29 +03:00
2036cc117f Fix support app popup buttons 2019-09-01 23:19:26 +03:00
389d28a1e1 Location manager optimizations 2019-09-01 23:12:43 +03:00
27e6198d83 Remove test data send 2019-09-01 22:31:46 +03:00
de762a4878 Resolves #401 Fix login restart and several login views opened 2019-09-01 22:27:56 +03:00
e8efefe25d Remove test data send 2019-09-01 22:01:27 +03:00
21f3e8985a 0.6.5-alpha1 2019-09-01 00:30:27 +03:00
622543d405 Fix notification handlers return type 2019-09-01 00:14:54 +03:00
abdc0fc1c8 Remove dead code 2019-09-01 00:12:16 +03:00
1ecb839042 Add user messages 2019-08-31 23:55:32 +03:00
cece4d1e16 Add location tracking switch disabled by default 2019-08-31 23:09:30 +03:00
623634cb6e Send location on app resume 2019-08-31 22:37:55 +03:00
f9c37f5084 Refactor ConnectionManager and DeviceInfoManager 2019-08-31 22:10:07 +03:00
3e12f4f8a4 Create MobileAppIntegrationManager 2019-08-31 22:06:52 +03:00
b07ff6fe71 WIP #49 Add location update interval settings 2019-08-30 21:45:34 +03:00
5a3b57c28e Update location permissions 2019-08-30 19:56:22 +03:00
e858eee83b Merge pull request #396 from koying/history_tweak
History tweaks
2019-08-30 19:48:04 +03:00
73f00d3bd7 Merge pull request #428 from estevez-dev/gelocator_plugin
WIP #49 - geolocator plugin testing
2019-08-30 19:46:25 +03:00
eea59cf11b WIP #49 - geolocator plugin testing 2019-08-30 16:12:03 +03:00
61b459ed8a WIP #49 2019-08-30 15:51:39 +03:00
dca8c309aa WIP #49 Location tracking service and test alarm manager 2019-08-30 15:04:51 +03:00
be53500104 WIP #49 2019-08-29 12:57:24 +03:00
bc1a791608 WIP #49 2019-08-28 19:23:24 +03:00
b112ff980a Update README.md 2019-08-28 00:08:17 +03:00
7beab9ae93 0.6.4 2019-08-27 20:06:18 +03:00
8c0d1f90a3 Resolves #422 Fix noSuchMethod for group-based UI 2019-08-27 20:05:33 +03:00
05c05ba768 Possibly resolves #422 2019-08-27 12:05:33 +03:00
67e885e76a Update subscribtions 2019-08-26 19:42:26 +03:00
594bce0b8d Update packages 2019-08-26 19:00:17 +03:00
7f6569e0db 0.6.3 2019-08-26 18:56:06 +03:00
1c829c8364 Remove sensitive information from log 2019-08-26 18:55:12 +03:00
7ca4b02e6d Resolves#413 Handling removed mobile_app integration 2019-08-26 18:30:49 +03:00
fadfefd836 Resolves #421 Manual long-lived token 2019-08-26 18:04:40 +03:00
37155901ef Resolves #205, Resolves #417 Condotional cards support 2019-08-26 17:03:28 +03:00
fbbb96409d Project structure change in progress 2019-08-24 21:22:32 +03:00
5126c54914 Resolves #412 Add 'Support app development' option 2019-08-24 20:39:25 +03:00
916d0b7e3c WIP #412 2019-08-24 15:22:23 +03:00
0815840a9c WIP #412 2019-08-23 14:13:58 +03:00
bc237796b2 0.6.2 2019-08-21 21:30:57 +03:00
7f44800f64 Fix mobile_app registration and update 2019-08-21 21:30:11 +03:00
85ac746e9d 0.6.1 2019-08-21 18:49:56 +03:00
8215175098 Resolves #409 Remove slesh from the end of HA address 2019-08-21 18:48:53 +03:00
39ee8b1799 Resolves #410 os_version removed from mobile_app registration 2019-08-21 18:40:16 +03:00
c76d3d68c8 Resolves #406 Minimum light brightness is set to 1 isntead of 0 2019-08-21 18:28:07 +03:00
cde257922b 0.6.0 2019-08-21 09:40:18 +03:00
be0c9d3372 App bundle test 2019-08-20 11:22:47 +03:00
66cd7ea307 0.6.0-alpha3 2019-08-16 15:05:43 +03:00
b704ce6984 0.6.0-alpha3 2019-08-16 14:01:10 +03:00
247c856a41 Resolves #397 Add default icon for device_tracker 2019-08-16 13:44:29 +03:00
9afaebfa12 Resolves #401 Climate support fixes 2019-08-16 13:29:41 +03:00
929abea5d3 Login and mobile app registration improvements 2019-08-16 12:32:36 +03:00
13102a6b04 CHG: [history] wrap around 2019-06-27 18:25:36 +02:00
57c3083f9f CHG: [history widget] select last measurement initally 2019-06-27 18:25:24 +02:00
5c31ddd00f Resolves #345 Add default icon for Remote 2019-06-23 16:19:28 +03:00
8f55be187d Resolves #324 devider fix, entity card padding fix 2019-06-23 16:08:12 +03:00
1fe82d8b0d Resolves #334 Fix plug device_class icons 2019-06-23 15:27:55 +03:00
cbc56a8105 Resolves #336 Replace 'unknown' state with '-'. Show displayState for badges 2019-06-23 15:24:08 +03:00
b63cddfa46 Resolves #330 Add Help menu item 2019-06-23 15:15:33 +03:00
91db82f730 Resolves #331 Menu item text change 2019-06-23 15:11:04 +03:00
0c4d1b78ff Resolves #323 fix widget padding for entity page 2019-06-23 15:09:18 +03:00
5af2fd0562 Resolves #376 Dynamic font size on badges 2019-06-23 14:53:11 +03:00
2375543ebf Fix camera stream open 2019-06-23 14:36:15 +03:00
de187f3ed5 Update mdi array builder script 2019-06-21 21:30:26 +03:00
9266ffacf3 Update Material Design Icons font to 3.6.95 2019-06-21 21:28:37 +03:00
3c0ca5d16d Resolve #382 VIew camera in chrome custom tab 2019-06-21 21:01:53 +03:00
caabf25260 WIP #382 Open camera stream in CHrome custom tab 2019-06-21 14:29:56 +03:00
0af2afbb80 Add links to web version of COnfiguration secrtions 2019-06-21 13:33:28 +03:00
12d226509d App registration improvements 2019-06-21 13:21:30 +03:00
3417c38426 Resolves #386 2019-06-21 12:53:03 +03:00
c7fc5afbb8 Resolves #389 Improve app registration checking 2019-06-21 12:39:58 +03:00
11f565a9dc Resolves #388 2019-06-21 12:05:55 +03:00
53240faac3 Fix automatic OAuth window open issue 2019-06-16 22:57:50 +03:00
95d4878785 Resolves #48 Native notifications 2019-06-16 20:08:50 +03:00
ef15026203 Fix authentication process. App register in background 2019-06-16 16:32:55 +03:00
ad6355503b WIP #48 Show dialog on app registration 2019-06-16 00:23:11 +03:00
491c2b0dc0 WIP #48 Notifications with mobile_app component 2019-06-16 00:08:13 +03:00
5b99ade088 Resolves #318 add mobile_app integration 2019-06-15 18:07:11 +03:00
e1d9d9f304 Stop connection init if settings is empty 2019-06-15 14:36:11 +03:00
209ccd4f7f New error class 2019-04-19 21:43:52 +03:00
5a8a207f2e minor fix 2019-04-19 14:40:05 +03:00
19c85d9c16 Don't handle state change if fetch is in progress 2019-04-19 14:38:02 +03:00
a916ddfa50 Resolves #364, Resolves #363 Connection issues 2019-04-19 14:07:44 +03:00
8c1ad9c7f9 Fix login button 2019-04-05 14:07:03 +03:00
93af1eca7e Resolves #355 Add login button on empty screen 2019-04-05 13:39:54 +03:00
cabf836fa3 WIP #355 Disconnect when logout 2019-04-05 13:06:14 +03:00
15b3d31a6f Resolves #353 Show error if connection drops 2019-04-05 12:23:31 +03:00
9b98689012 Fix connection error handling 2019-04-05 12:08:32 +03:00
84ebd0c33c Resolves #352 Fix panels clear after logout 2019-04-05 11:59:13 +03:00
ccd7774931 Resolves #350 Fix displayed hostname 2019-04-05 11:57:58 +03:00
b2773635f5 Connection improvements 2019-04-05 11:48:41 +03:00
8b046b7313 Merge branch '0.6.0-alpha1-1' 2019-04-04 22:25:19 +03:00
885a516676 alpha2 2019-04-04 22:12:08 +03:00
921b0e09b0 Merge branch 'terms_and_privacy' into 0.6.0-alpha1-1 2019-04-04 22:10:29 +03:00
277c67fc6f Add padding for links in About dialog 2019-04-04 21:54:41 +03:00
2a01ff8a03 Bump version in UI 2019-04-04 21:51:05 +03:00
b246b7bc1d 0.5.3 and new build numbers 2019-04-04 21:44:16 +03:00
e1868b9a14 Add privacy polici and terms and conditions links 2019-04-04 21:43:23 +03:00
125f3ac16c Resolves #327 Timer duration parsing error 2019-04-04 21:38:23 +03:00
be502b5668 Discord icon fix 2019-04-04 21:38:05 +03:00
6f33fdca9f New app icon 2019-04-04 21:37:41 +03:00
a7cda2a35e WIP #48 Notifications 2019-03-30 00:29:52 +02:00
102b10ade0 WIP #48 Notifications 2019-03-29 13:09:34 +02:00
111 changed files with 5921 additions and 2034 deletions

4
.gitignore vendored
View File

@ -10,4 +10,6 @@ build/
.idea/
key.properties
key.properties
premium_features_manager.class.dart
pubspec.lock

View File

@ -1,12 +1,9 @@
[![flutter](https://somegeeky.website/assets/badges/flutter_badge_v3.svg)](https://somegeeky.website/badges/flutter) [![dart](https://somegeeky.website/assets/badges/dart_badge_v3.svg)](https://somegeeky.website/badges/dart)
# HA Client
## Native Android client for Home Assistant
### With Lovelace UI support
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912)
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)

View File

@ -6,6 +6,11 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
@ -13,10 +18,15 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:name=".Application"
android:label="HA Client"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="ha_notify" />
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
@ -27,10 +37,10 @@
<!-- 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). -->
defined in @style/LaunchTheme).
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
android:value="true" />-->
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
@ -40,5 +50,20 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<receiver
android:name="io.flutter.plugins.androidalarmmanager.AlarmBroadcastReceiver"
android:exported="false"/>
<receiver
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
</intent-filter>
</receiver>
</application>
</manifest>

View File

@ -0,0 +1,18 @@
package com.keyboardcrumbs.hassclient;
import io.flutter.app.FlutterApplication;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
import io.flutter.plugins.GeneratedPluginRegistrant;
public class Application extends FlutterApplication implements PluginRegistrantCallback {
@Override
public void onCreate() {
super.onCreate();
}
@Override
public void registerWith(PluginRegistry registry) {
GeneratedPluginRegistrant.registerWith(registry);
}
}

View File

@ -3,8 +3,9 @@ package com.keyboardcrumbs.hassclient;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugins.share.FlutterShareReceiverActivity;
public class MainActivity extends FlutterActivity {
public class MainActivity extends FlutterShareReceiverActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

1232
android/hs_err_pid766.log Normal file

File diff suppressed because it is too large Load Diff

16
assets/js/externalAuth.js Normal file
View File

@ -0,0 +1,16 @@
window.externalApp = {};
window.externalApp.getExternalAuth = function(options) {
console.log("Starting external auth");
var options = JSON.parse(options);
if (options && options.callback) {
var responseData = {
access_token: "[token]",
expires_in: 1800
};
console.log("Waiting for callback to be added");
setTimeout(function(){
console.log("Calling a callback");
window[options.callback](true, responseData);
}, 500);
}
};

41
flutter_01.log Normal file
View File

@ -0,0 +1,41 @@
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
## command
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
## exception
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/.packages, isolateId: isolates/68989666}}, details: Isolate must be runnable before this request is made.}}
```
null```
## flutter doctor
```
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
• Engine revision fee001c93f
• Dart version 2.4.0
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
• Android SDK at /home/estevez/Android/Sdk
• Android NDK location not configured (optional; useful for native profiling support)
• Platform android-29, build-tools 29.0.2
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
• All Android licenses accepted.
[✓] Android Studio (version 3.5)
• Android Studio at /home/estevez/bin/android-studio
• Flutter plugin version 38.2.3
• Dart plugin version 191.8423
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
[✓] Connected device (1 available)
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
• No issues found!
```

41
flutter_02.log Normal file
View File

@ -0,0 +1,41 @@
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
## command
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
## exception
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/.packages, isolateId: isolates/289688365}}, details: Isolate must be runnable before this request is made.}}
```
null```
## flutter doctor
```
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
• Engine revision fee001c93f
• Dart version 2.4.0
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
• Android SDK at /home/estevez/Android/Sdk
• Android NDK location not configured (optional; useful for native profiling support)
• Platform android-29, build-tools 29.0.2
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
• All Android licenses accepted.
[✓] Android Studio (version 3.5)
• Android Studio at /home/estevez/bin/android-studio
• Flutter plugin version 38.2.3
• Dart plugin version 191.8423
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
[✓] Connected device (1 available)
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
• No issues found!
```

41
flutter_03.log Normal file
View File

@ -0,0 +1,41 @@
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
## command
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
## exception
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/.packages, isolateId: isolates/866521062}}, details: Isolate must be runnable before this request is made.}}
```
null```
## flutter doctor
```
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
• Engine revision fee001c93f
• Dart version 2.4.0
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
• Android SDK at /home/estevez/Android/Sdk
• Android NDK location not configured (optional; useful for native profiling support)
• Platform android-29, build-tools 29.0.2
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
• All Android licenses accepted.
[✓] Android Studio (version 3.5)
• Android Studio at /home/estevez/bin/android-studio
• Flutter plugin version 38.2.3
• Dart plugin version 191.8423
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
[✓] Connected device (1 available)
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
• No issues found!
```

View File

@ -13,7 +13,12 @@ class HACard {
int columnsCount;
List stateFilter;
List states;
List conditions;
String content;
String unit;
int min;
int max;
Map severity;
HACard({
this.name,
@ -26,8 +31,17 @@ class HACard {
this.showEmpty: true,
this.content,
this.states,
this.conditions: const [],
this.unit,
this.min,
this.max,
this.severity,
@required this.type
});
}) {
if (this.columnsCount <= 0) {
this.columnsCount = 4;
}
}
List<EntityWrapper> getEntitiesToShow() {
return entities.where((entityWrapper) {

View File

@ -24,33 +24,58 @@ class CardWidget extends StatelessWidget {
}
}
if (card.conditions.isNotEmpty) {
bool showCardByConditions = true;
for (var condition in card.conditions) {
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
if (conditionEntity != null &&
((condition['state'] != null && conditionEntity.state != condition['state']) ||
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
) {
showCardByConditions = false;
break;
}
}
if (!showCardByConditions) {
return Container(width: 0.0, height: 0.0,);
}
}
switch (card.type) {
case CardType.entities: {
case CardType.ENTITIES: {
return _buildEntitiesCard(context);
}
case CardType.glance: {
case CardType.GLANCE: {
return _buildGlanceCard(context);
}
case CardType.mediaControl: {
case CardType.MEDIA_CONTROL: {
return _buildMediaControlsCard(context);
}
case CardType.entityButton: {
case CardType.ENTITY_BUTTON: {
return _buildEntityButtonCard(context);
}
case CardType.markdown: {
case CardType.GAUGE: {
return _buildGaugeCard(context);
}
/* case CardType.LIGHT: {
return _buildLightCard(context);
}*/
case CardType.MARKDOWN: {
return _buildMarkdownCard(context);
}
case CardType.alarmPanel: {
case CardType.ALARM_PANEL: {
return _buildAlarmPanelCard(context);
}
case CardType.horizontalStack: {
case CardType.HORIZONTAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
@ -73,7 +98,7 @@ class CardWidget extends StatelessWidget {
return Container(height: 0.0, width: 0.0,);
}
case CardType.verticalStack: {
case CardType.VERTICAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
@ -107,12 +132,12 @@ class CardWidget extends StatelessWidget {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name));
body.add(CardHeader(name: card.name));
entitiesToShow.forEach((EntityWrapper entity) {
if (!entity.entity.isHidden) {
body.add(
Padding(
padding: EdgeInsets.fromLTRB(10.0, 4.0, 0.0, 4.0),
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
child: EntityModel(
entityWrapper: entity,
handleTap: true,
@ -122,7 +147,10 @@ class CardWidget extends StatelessWidget {
}
});
return Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
child: Padding(
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding),
child: Column(mainAxisSize: MainAxisSize.min, children: body),
)
);
}
@ -131,7 +159,7 @@ class CardWidget extends StatelessWidget {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name));
body.add(CardHeader(name: card.name));
body.add(MarkdownBody(data: card.content));
return Card(
child: Padding(
@ -143,7 +171,7 @@ class CardWidget extends StatelessWidget {
Widget _buildAlarmPanelCard(BuildContext context) {
List<Widget> body = [];
body.add(CardHeaderWidget(
body.add(CardHeader(
name: card.name ?? "",
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
style: TextStyle(
@ -195,39 +223,51 @@ class CardWidget extends StatelessWidget {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> rows = [];
rows.add(CardHeaderWidget(name: card.name));
rows.add(CardHeader(name: card.name));
List<Widget> result = [];
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
entitiesToShow.forEach((EntityWrapper entity) {
result.add(
FractionallySizedBox(
widthFactor: 1/columnsCount,
child: EntityModel(
entityWrapper: entity,
child: GlanceEntityContainer(
showName: card.showName,
showState: card.showState,
),
handleTap: true
),
)
);
});
rows.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding),
child: Wrap(
//alignment: WrapAlignment.spaceAround,
runSpacing: Sizes.rowPadding*2,
children: result,
padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding),
child: FractionallySizedBox(
widthFactor: 1,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
List<Widget> buttons = [];
double buttonWidth = constraints.maxWidth / columnsCount;
entitiesToShow.forEach((EntityWrapper entity) {
buttons.add(
SizedBox(
width: buttonWidth,
child: EntityModel(
entityWrapper: entity,
child: GlanceCardEntityContainer(
showName: card.showName,
showState: card.showState,
),
handleTap: true
),
)
);
});
return Wrap(
//spacing: 5.0,
//alignment: WrapAlignment.spaceEvenly,
runSpacing: Sizes.doubleRowPadding,
children: buttons,
);
}
),
),
)
);
return Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: rows)
child: Column(
mainAxisSize: MainAxisSize.min,
children: rows
)
);
}
@ -247,7 +287,41 @@ class CardWidget extends StatelessWidget {
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: ButtonEntityContainer(),
child: EntityButtonCardBody(),
handleTap: true
)
);
}
Widget _buildGaugeCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name ??
card.linkedEntityWrapper.displayName;
card.linkedEntityWrapper.unitOfMeasurement = card.unit ??
card.linkedEntityWrapper.unitOfMeasurement;
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: GaugeCardBody(
min: card.min,
max: card.max,
severity: card.severity,
),
handleTap: true
)
);
}
Widget _buildLightCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name ??
card.linkedEntityWrapper.displayName;
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: LightCardBody(
min: card.min,
max: card.max,
severity: card.severity,
),
handleTap: true
)
);
@ -255,7 +329,7 @@ class CardWidget extends StatelessWidget {
Widget _buildUnsupportedCard(BuildContext context) {
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name ?? ""));
body.add(CardHeader(name: card.name ?? ""));
List<Widget> result = [];
if (card.linkedEntityWrapper != null) {
result.addAll(<Widget>[
@ -286,4 +360,4 @@ class CardWidget extends StatelessWidget {
);
}
}
}

View File

@ -1,12 +1,12 @@
part of '../main.dart';
part of '../../main.dart';
class CardHeaderWidget extends StatelessWidget {
class CardHeader extends StatelessWidget {
final String name;
final Widget trailing;
final Widget subtitle;
const CardHeaderWidget({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
@override
Widget build(BuildContext context) {

View File

@ -1,8 +1,8 @@
part of '../main.dart';
part of '../../main.dart';
class ButtonEntityContainer extends StatelessWidget {
class EntityButtonCardBody extends StatelessWidget {
ButtonEntityContainer({
EntityButtonCardBody({
Key key,
}) : super(key: key);
@ -15,24 +15,25 @@ class ButtonEntityContainer extends StatelessWidget {
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FractionallySizedBox(
widthFactor: 0.4,
child: FittedBox(
fit: BoxFit.fitHeight,
child: EntityIcon(
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
size: Sizes.iconSize,
)
child: FractionallySizedBox(
widthFactor: 1,
child: Column(
children: <Widget>[
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return EntityIcon(
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
size: constraints.maxWidth / 2.5,
);
}
),
),
_buildName()
],
_buildName()
],
),
),
);
}

View File

@ -0,0 +1,153 @@
part of '../../main.dart';
class GaugeCardBody extends StatefulWidget {
final int min;
final int max;
final Map severity;
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
@override
_GaugeCardBodyState createState() => _GaugeCardBodyState();
}
class _GaugeCardBodyState extends State<GaugeCardBody> {
List<charts.Series> seriesList;
List<charts.Series<GaugeSegment, String>> _createData(double value) {
double fixedValue;
if (value > widget.max) {
fixedValue = widget.max.toDouble();
} else if (value < widget.min) {
fixedValue = widget.min.toDouble();
} else {
fixedValue = value;
}
double toShow = ((fixedValue - widget.min) / (widget.max - widget.min)) * 100;
Color mainColor;
if (widget.severity != null) {
if (widget.severity["red"] is int && fixedValue >= widget.severity["red"]) {
mainColor = Colors.red;
} else if (widget.severity["yellow"] is int && fixedValue >= widget.severity["yellow"]) {
mainColor = Colors.amber;
} else {
mainColor = Colors.green;
}
} else {
mainColor = Colors.green;
}
final data = [
GaugeSegment('Main', toShow, mainColor),
GaugeSegment('Rest', 100 - toShow, Colors.black45),
];
return [
charts.Series<GaugeSegment, String>(
id: 'Segments',
domainFn: (GaugeSegment segment, _) => segment.segment,
measureFn: (GaugeSegment segment, _) => segment.value,
colorFn: (GaugeSegment segment, _) => segment.color,
// Set a label accessor to control the text of the arc label.
labelAccessorFn: (GaugeSegment segment, _) =>
segment.segment == 'Main' ? '${segment.value}' : null,
data: data,
)
];
}
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
child: AspectRatio(
aspectRatio: 1.5,
child: Stack(
fit: StackFit.expand,
overflow: Overflow.clip,
children: [
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double verticalOffset;
if(constraints.maxWidth > 150.0) {
verticalOffset = 0.2;
} else if (constraints.maxWidth > 100.0) {
verticalOffset = 0.3;
} else {
verticalOffset = 0.3;
}
return FractionallySizedBox(
heightFactor: 2,
widthFactor: 1,
alignment: FractionalOffset(0,verticalOffset),
child: charts.PieChart(
_createData(entityWrapper.entity.doubleState),
animate: false,
defaultRenderer: charts.ArcRendererConfig(
arcRatio: 0.4,
startAngle: pi,
arcLength: pi,
),
),
);
}
),
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: 2*fontSize),
child: SimpleEntityState(
//textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
bold: true,
textAlign: TextAlign.center,
padding: EdgeInsets.all(0.0),
fontSize: fontSize,
//padding: EdgeInsets.only(top: Sizes.rowPadding),
),
);
}
),
),
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: fontSize),
child: EntityName(
fontSize: fontSize,
maxLines: 1,
padding: EdgeInsets.all(0.0),
textAlign: TextAlign.center,
textOverflow: TextOverflow.ellipsis,
),
);
}
),
)
]
)
),
);
}
}
class GaugeSegment {
final String segment;
final double value;
final charts.Color color;
GaugeSegment(this.segment, this.value, Color color)
: this.color = charts.Color(
r: color.red, g: color.green, b: color.blue, a: color.alpha);
}

View File

@ -1,6 +1,6 @@
part of '../main.dart';
part of '../../main.dart';
class GlanceEntityContainer extends StatelessWidget {
class GlanceCardEntityContainer extends StatelessWidget {
final bool showName;
final bool showState;
@ -9,7 +9,7 @@ class GlanceEntityContainer extends StatelessWidget {
final double nameFontSize;
final bool wordsWrapInName;
GlanceEntityContainer({
GlanceCardEntityContainer({
Key key,
@required this.showName,
@required this.showState,
@ -39,10 +39,10 @@ class GlanceEntityContainer extends StatelessWidget {
}
}
result.add(
EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
)
EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
)
);
if (!nameInTheBottom) {
if (showState) {
@ -54,14 +54,9 @@ class GlanceEntityContainer extends StatelessWidget {
return Center(
child: InkResponse(
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: Sizes.iconSize * 2),
child: Column(
mainAxisSize: MainAxisSize.min,
//mainAxisAlignment: MainAxisAlignment.start,
//crossAxisAlignment: CrossAxisAlignment.center,
children: result,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: result,
),
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),

View File

@ -0,0 +1,90 @@
part of '../../main.dart';
class LightCardBody extends StatefulWidget {
final int min;
final int max;
final Map severity;
LightCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
@override
_LightCardBodyState createState() => _LightCardBodyState();
}
class _LightCardBodyState extends State<LightCardBody> {
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
LightEntity entity = entityWrapper.entity;
Logger.d("Light brightness: ${entity.brightness}");
return FractionallySizedBox(
widthFactor: 0.5,
child: Container(
//color: Colors.redAccent,
child: SingleCircularSlider(
255,
entity.brightness ?? 0,
baseColor: Colors.white,
handlerColor: Colors.blue[200],
selectionColor: Colors.blue[100],
),
),
);
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
child: AspectRatio(
aspectRatio: 1.5,
child: Stack(
fit: StackFit.expand,
overflow: Overflow.clip,
children: [
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: 2*fontSize),
child: SimpleEntityState(
//textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
bold: true,
textAlign: TextAlign.center,
padding: EdgeInsets.all(0.0),
fontSize: fontSize,
//padding: EdgeInsets.only(top: Sizes.rowPadding),
),
);
}
),
),
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: fontSize),
child: EntityName(
fontSize: fontSize,
maxLines: 1,
padding: EdgeInsets.all(0.0),
textAlign: TextAlign.center,
textOverflow: TextOverflow.ellipsis,
),
);
}
),
)
]
)
),
);
}
}

View File

@ -1,343 +0,0 @@
part of 'main.dart';
class Connection {
static final Connection _instance = Connection._internal();
factory Connection() {
return _instance;
}
Connection._internal();
String displayHostname;
String _webSocketAPIEndpoint;
String httpWebHost;
String _token;
String _tempToken;
String oauthUrl;
bool get isAuthenticated => _token != null;
StreamSubscription _socketSubscription;
Duration connectTimeout = Duration(seconds: 15);
bool isConnected = false;
var onStateChangeCallback;
IOWebSocketChannel _socket;
int _currentMessageId = 0;
Map<String, Completer> _messageResolver = {};
Future init(onStateChange) async {
Completer completer = Completer();
onStateChangeCallback = onStateChange;
SharedPreferences prefs = await SharedPreferences.getInstance();
String domain = prefs.getString('hassio-domain');
String port = prefs.getString('hassio-port');
displayHostname = "$domain:$port";
_webSocketAPIEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
httpWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
//_token = prefs.getString('hassio-token');
final storage = new FlutterSecureStorage();
try {
_token = await storage.read(key: "hacl_llt");
} catch (e) {
Logger.e("Cannt read secure storage. Need to relogin.");
_token = null;
await storage.delete(key: "hacl_llt");
}
if ((domain == null) || (port == null) ||
(domain.length == 0) || (port.length == 0)) {
completer.completeError({"errorCode": 5, "errorMessage": "Check connection settings"});
} else {
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}&redirect_uri=${Uri.encodeComponent('http://ha-client.homemade.systems/service/auth_callback.html')}";
if (_token == null) {
await AuthManager().getTempToken(
httpWebHost: httpWebHost,
oauthUrl: oauthUrl
).then((token) {
Logger.d("Token from AuthManager recived");
_tempToken = token;
});
}
_connect().timeout(connectTimeout, onTimeout: () {
_disconnect().then((_) {
completer.completeError(
{"errorCode": 1, "errorMessage": "Connection timeout"});
});
}).then((_) => completer.complete()).catchError((e) {
completer.completeError(e);
});
}
return completer.future;
}
Completer connecting;
Future _connect() async {
if (connecting != null && !connecting.isCompleted) {
Logger.w("");
return connecting.future;
}
connecting = Completer();
await _disconnect();
Logger.d( "Socket connecting...");
_socket = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
_socketSubscription = _socket.stream.listen(
(message) {
isConnected = true;
var data = json.decode(message);
if (data["type"] == "auth_required") {
Logger.d("[Received] <== ${data.toString()}");
_authenticate().then((_) => connecting.complete()).catchError((e) {
if (!connecting.isCompleted) connecting.completeError(e);
});
} else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.complete();
_messageResolver.remove("auth");
if (!connecting.isCompleted) connecting.complete(sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "state_changed"},
));
} else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.completeError({"errorCode": 62, "errorMessage": "${data["message"]}"});
_messageResolver.remove("auth");
logout().then((_) {
if (!connecting.isCompleted) connecting.completeError({"errorCode": 62, "errorMessage": "${data["message"]}"});
});
} else {
_handleMessage(data);
}
},
cancelOnError: true,
onDone: () => _handleSocketClose(connecting),
onError: (e) => _handleSocketError(e, connecting)
);
return connecting.future;
}
Future _disconnect() async {
Logger.d( "Socket disconnecting...");
await _socketSubscription?.cancel();
await _socket?.sink?.close()?.timeout(Duration(seconds: 4),
onTimeout: () => Logger.d( "Socket sink close timeout")
);
Logger.d( "..Disconnected");
}
_handleMessage(data) {
if (data["type"] == "result") {
if (data["id"] != null && data["success"]) {
Logger.d("[Received] <== Request id ${data['id']} was successful");
_messageResolver["${data["id"]}"]?.complete(data["result"]);
} else if (data["id"] != null) {
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
_messageResolver["${data["id"]}"]?.completeError({"errorMessage": "${data['error']["message"]}"});
}
_messageResolver.remove("${data["id"]}");
} else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
onStateChangeCallback(data["event"]["data"]);
} else if (data["event"] != null) {
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
} else {
Logger.e("Event is null: $data");
}
} else {
Logger.d("[Received unhandled] <== ${data.toString()}");
}
}
void _handleSocketClose(Completer connectionCompleter) {
isConnected = false;
Logger.d("Socket disconnected.");
if (!connectionCompleter.isCompleted) {
connectionCompleter.completeError({"errorCode": 82, "errorMessage": "Disconnected"});
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect();
});
});
}
}
void _handleSocketError(e, Completer connectionCompleter) {
isConnected = false;
Logger.e("Socket stream Error: $e");
if (!connectionCompleter.isCompleted) {
connectionCompleter.completeError({"errorCode": 81, "errorMessage": "Unable to connect to Home Assistant"});
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect();
});
});
}
}
Future _authenticate() {
Completer completer = Completer();
if (_token != null) {
Logger.d( "Long-lived token exist");
Logger.d( "[Sending] ==> auth request");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_token"},
auth: true
).then((_) {
completer.complete();
}).catchError((e) => completer.completeError(e));
} else if (_tempToken != null) {
Logger.d("We have temp token. Loging in...");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_tempToken"},
auth: true
).then((_) {
Logger.d("Requesting long-lived token...");
_getLongLivedToken().then((_) {
completer.complete();
}).catchError((e) {
Logger.e("Can't get long-lived token: $e");
throw e;
});
}).catchError((e) => completer.completeError(e));
} else {
completer.completeError({"errorCode": 63, "errorMessage": "General login error"});
}
return completer.future;
}
Future logout() {
_token = null;
_tempToken = null;
final storage = new FlutterSecureStorage();
return storage.delete(key: "hacl_llt");
}
Future _getLongLivedToken() {
Completer completer = Completer();
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
Logger.d("Got long-lived token.");
_token = data;
_tempToken = null;
final storage = new FlutterSecureStorage();
storage.write(key: "hacl_llt", value: "$_token").then((_) {
completer.complete();
}).catchError((e) {
throw e;
});
}).catchError((e) {
logout();
completer.completeError({"errorCode": 63, "errorMessage": "Authentication error: $e"});
});
return completer.future;
}
Future sendSocketMessage({String type, Map additionalData, bool auth: false}) {
Completer _completer = Completer();
Map dataObject = {"type": "$type"};
String callbackName;
if (!auth) {
_incrementMessageId();
dataObject["id"] = _currentMessageId;
callbackName = "$_currentMessageId";
} else {
callbackName = "auth";
}
if (additionalData != null) {
dataObject.addAll(additionalData);
}
_messageResolver[callbackName] = _completer;
String rawMessage = json.encode(dataObject);
Logger.d("[Sending] ==> $rawMessage");
if (!isConnected) {
_connect().timeout(connectTimeout, onTimeout: (){
_completer.completeError({"errorCode": 8, "errorMessage": "No connection to Home Assistant"});
}).then((_) {
_socket.sink.add(rawMessage);
}).catchError((e) {
_completer.completeError(e);
});
} else {
_socket.sink.add(rawMessage);
}
return _completer.future;
}
void _incrementMessageId() {
_currentMessageId += 1;
}
Future callService({String domain, String service, String entityId, Map additionalServiceData}) {
Map serviceData = {};
if (entityId != null) {
serviceData["entity_id"] = entityId;
}
if (additionalServiceData != null && additionalServiceData.isNotEmpty) {
serviceData.addAll(additionalServiceData);
}
if (serviceData.isNotEmpty)
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
else
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
}
Future<List> getHistory(String entityId) async {
DateTime now = DateTime.now();
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String url = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
Logger.d("[Sending] ==> $url");
http.Response historyResponse;
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_token",
"Content-Type": "application/json"
});
var history = json.decode(historyResponse.body);
if (history is List) {
Logger.d( "[Received] <== ${history.first.length} history recors");
return history;
} else {
return [];
}
}
Future sendHTTPPost({String host, String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true, String authToken}) async {
Completer completer = Completer();
String url = "$host$endPoint";
Logger.d("[Sending] ==> $url");
Map<String, String> headers = {};
if (contentType != null) {
headers["Content-Type"] = contentType;
}
if (includeAuthHeader) {
headers["authorization"] = "Bearer $authToken";
}
http.post(
url,
headers: headers,
body: data
).then((response) {
Logger.d("[Received] <== ${response.statusCode}, ${response.body}");
if (response.statusCode == 200) {
completer.complete(response.body);
} else {
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
}
}).catchError((e) {
completer.completeError(e);
});
return completer.future;
}
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of 'main.dart';
class EntityState {
static const on = 'on';
@ -77,23 +77,40 @@ class EntityUIAction {
}
class CardType {
static const horizontalStack = "horizontal-stack";
static const verticalStack = "vertical-stack";
static const entities = "entities";
static const glance = "glance";
static const mediaControl = "media-control";
static const weatherForecast = "weather-forecast";
static const thermostat = "thermostat";
static const sensor = "sensor";
static const plantStatus = "plant-status";
static const pictureEntity = "picture-entity";
static const pictureElements = "picture-elements";
static const picture = "picture";
static const map = "map";
static const iframe = "iframe";
static const gauge = "gauge";
static const entityButton = "entity-button";
static const conditional = "conditional";
static const alarmPanel = "alarm-panel";
static const markdown = "markdown";
static const HORIZONTAL_STACK = "horizontal-stack";
static const VERTICAL_STACK = "vertical-stack";
static const ENTITIES = "entities";
static const GLANCE = "glance";
static const MEDIA_CONTROL = "media-control";
static const WEATHER_FORECAST = "weather-forecast";
static const THERMOSTAT = "thermostat";
static const SENSOR = "sensor";
static const PLANT_STATUS = "plant-status";
static const PICTURE_ENTITY = "picture-entity";
static const PICTURE_ELEMENTS = "picture-elements";
static const PICTURE = "picture";
static const MAP = "map";
static const IFRAME = "iframe";
static const GAUGE = "gauge";
static const ENTITY_BUTTON = "entity-button";
static const CONDITIONAL = "conditional";
static const ALARM_PANEL = "alarm-panel";
static const MARKDOWN = "markdown";
static const LIGHT = "light";
}
class Sizes {
static const rightWidgetPadding = 10.0;
static const leftWidgetPadding = 10.0;
static const buttonPadding = 4.0;
static const extendedWidgetHeight = 50.0;
static const iconSize = 28.0;
static const largeIconSize = 46.0;
static const stateFontSize = 15.0;
static const nameFontSize = 15.0;
static const smallFontSize = 14.0;
static const largeFontSize = 24.0;
static const inputWidth = 160.0;
static const rowPadding = 10.0;
static const doubleRowPadding = rowPadding*2;
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class AlarmControlPanelEntity extends Entity {
AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class AlarmControlPanelControlsWidget extends StatefulWidget {

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class AutomationEntity extends Entity {
AutomationEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class ButtonEntity extends Entity {
ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class CameraEntity extends Entity {

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class ClimateEntity extends Entity {
@ -10,71 +10,57 @@ class ClimateEntity extends Entity {
);
static const SUPPORT_TARGET_TEMPERATURE = 1;
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
static const SUPPORT_TARGET_HUMIDITY = 8;
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
static const SUPPORT_FAN_MODE = 64;
static const SUPPORT_OPERATION_MODE = 128;
static const SUPPORT_HOLD_MODE = 256;
static const SUPPORT_SWING_MODE = 512;
static const SUPPORT_AWAY_MODE = 1024;
static const SUPPORT_AUX_HEAT = 2048;
static const SUPPORT_ON_OFF = 4096;
static const SUPPORT_TARGET_TEMPERATURE_RANGE = 2;
static const SUPPORT_TARGET_HUMIDITY = 4;
static const SUPPORT_FAN_MODE = 8;
static const SUPPORT_PRESET_MODE = 16;
static const SUPPORT_SWING_MODE = 32;
static const SUPPORT_AUX_HEAT = 64;
//static const SUPPORT_OPERATION_MODE = 16;
//static const SUPPORT_HOLD_MODE = 256;
//static const SUPPORT_AWAY_MODE = 1024;
//static const SUPPORT_ON_OFF = 4096;
ClimateEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportTargetTemperature => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
bool get supportTargetTemperatureHigh => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
bool get supportTargetTemperatureLow => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
bool get supportTargetTemperatureRange => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE);
bool get supportTargetHumidity => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
bool get supportTargetHumidityHigh => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
bool get supportTargetHumidityLow => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
bool get supportFanMode =>
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
ClimateEntity.SUPPORT_FAN_MODE);
bool get supportOperationMode => ((supportedFeatures &
ClimateEntity.SUPPORT_OPERATION_MODE) ==
ClimateEntity.SUPPORT_OPERATION_MODE);
bool get supportHoldMode =>
((supportedFeatures & ClimateEntity.SUPPORT_HOLD_MODE) ==
ClimateEntity.SUPPORT_HOLD_MODE);
bool get supportSwingMode =>
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
ClimateEntity.SUPPORT_SWING_MODE);
bool get supportAwayMode =>
((supportedFeatures & ClimateEntity.SUPPORT_AWAY_MODE) ==
ClimateEntity.SUPPORT_AWAY_MODE);
bool get supportPresetMode =>
((supportedFeatures & ClimateEntity.SUPPORT_PRESET_MODE) ==
ClimateEntity.SUPPORT_PRESET_MODE);
bool get supportAuxHeat =>
((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) ==
ClimateEntity.SUPPORT_AUX_HEAT);
bool get supportOnOff =>
((supportedFeatures & ClimateEntity.SUPPORT_ON_OFF) ==
ClimateEntity.SUPPORT_ON_OFF);
List<String> get operationList => attributes["operation_list"] != null
? (attributes["operation_list"] as List).cast<String>()
List<String> get hvacModes => attributes["hvac_modes"] != null
? (attributes["hvac_modes"] as List).cast<String>()
: null;
List<String> get fanList => attributes["fan_list"] != null
? (attributes["fan_list"] as List).cast<String>()
List<String> get fanModes => attributes["fan_modes"] != null
? (attributes["fan_modes"] as List).cast<String>()
: null;
List<String> get swingList => attributes["swing_list"] != null
? (attributes["swing_list"] as List).cast<String>()
List<String> get presetModes => attributes["preset_modes"] != null
? (attributes["preset_modes"] as List).cast<String>()
: null;
List<String> get swingModes => attributes["swing_modes"] != null
? (attributes["swing_modes"] as List).cast<String>()
: null;
double get temperature => _getDoubleAttributeValue('temperature');
double get currentTemperature => _getDoubleAttributeValue('current_temperature');
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
double get targetLow => _getDoubleAttributeValue('target_temp_low');
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
@ -83,11 +69,12 @@ class ClimateEntity extends Entity {
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
double get minHumidity => _getDoubleAttributeValue('min_humidity');
double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5;
String get operationMode => attributes['operation_mode'];
String get hvacAction => attributes['hvac_action'];
String get fanMode => attributes['fan_mode'];
String get presetMode => attributes['preset_mode'];
String get swingMode => attributes['swing_mode'];
bool get awayMode => attributes['away_mode'] == "on";
bool get isOff => state == EntityState.off;
//bool get isOff => state == EntityState.off;
bool get auxHeat => attributes['aux_heat'] == "on";
@override
@ -96,10 +83,8 @@ class ClimateEntity extends Entity {
if (supportTargetTemperature) {
historyConfig.numericAttributesToShow.add("temperature");
}
if (supportTargetTemperatureHigh) {
if (supportTargetTemperatureRange) {
historyConfig.numericAttributesToShow.add("target_temp_high");
}
if (supportTargetTemperatureLow) {
historyConfig.numericAttributesToShow.add("target_temp_low");
}
}

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class ClimateControlWidget extends StatefulWidget {
@ -19,22 +19,22 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
double _tmpTargetLow = 0.0;
double _tmpTargetHigh = 0.0;
double _tmpTargetHumidity = 0.0;
String _tmpOperationMode;
String _tmpHVACMode;
String _tmpFanMode;
String _tmpSwingMode;
bool _tmpAwayMode = false;
bool _tmpIsOff = false;
String _tmpPresetMode;
//bool _tmpIsOff = false;
bool _tmpAuxHeat = false;
void _resetVars(ClimateEntity entity) {
_tmpTemperature = entity.temperature;
_tmpTargetHigh = entity.targetHigh;
_tmpTargetLow = entity.targetLow;
_tmpOperationMode = entity.operationMode;
_tmpHVACMode = entity.state;
_tmpFanMode = entity.fanMode;
_tmpSwingMode = entity.swingMode;
_tmpAwayMode = entity.awayMode;
_tmpIsOff = entity.isOff;
_tmpPresetMode = entity.presetMode;
//_tmpIsOff = entity.isOff;
_tmpAuxHeat = entity.auxHeat;
_tmpTargetHumidity = entity.targetHumidity;
@ -116,11 +116,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
});
}
void _setOperationMode(ClimateEntity entity, value) {
void _setHVACMode(ClimateEntity entity, value) {
setState(() {
_tmpOperationMode = value;
_tmpHVACMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_operation_mode", entity.entityId,{"operation_mode": "$_tmpOperationMode"}));
eventBus.fire(new ServiceCallEvent(entity.domain, "set_hvac_mode", entity.entityId,{"hvac_mode": "$_tmpHVACMode"}));
_resetStateTimer(entity);
});
}
@ -143,23 +143,23 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
});
}
void _setAwayMode(ClimateEntity entity, value) {
void _setPresetMode(ClimateEntity entity, value) {
setState(() {
_tmpAwayMode = value;
_tmpPresetMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_away_mode", entity.entityId,{"away_mode": "${_tmpAwayMode ? 'on' : 'off'}"}));
eventBus.fire(new ServiceCallEvent(entity.domain, "set_preset_mode", entity.entityId,{"preset_mode": "$_tmpPresetMode"}));
_resetStateTimer(entity);
});
}
void _setOnOf(ClimateEntity entity, value) {
/*void _setOnOf(ClimateEntity entity, value) {
setState(() {
_tmpIsOff = !value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
_resetStateTimer(entity);
});
}
}*/
void _setAuxHeat(ClimateEntity entity, value) {
setState(() {
@ -196,33 +196,34 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildOnOffControl(entity),
//_buildOnOffControl(entity),
_buildTemperatureControls(entity),
_buildTargetTemperatureControls(entity),
_buildHumidityControls(entity),
_buildOperationControl(entity),
_buildFanControl(entity),
_buildSwingControl(entity),
_buildAwayModeControl(entity),
_buildPresetModeControl(entity),
_buildAuxHeatControl(entity)
],
),
);
}
Widget _buildAwayModeControl(ClimateEntity entity) {
if (entity.supportAwayMode) {
return ModeSwitchWidget(
caption: "Away mode",
onChange: (value) => _setAwayMode(entity, value),
value: _tmpAwayMode,
Widget _buildPresetModeControl(ClimateEntity entity) {
if (entity.supportPresetMode) {
return ModeSelectorWidget(
options: entity.presetModes,
onChange: (mode) => _setPresetMode(entity, mode),
caption: "Preset",
value: _tmpPresetMode,
);
} else {
return Container(height: 0.0, width: 0.0,);
}
}
Widget _buildOnOffControl(ClimateEntity entity) {
/*Widget _buildOnOffControl(ClimateEntity entity) {
if (entity.supportOnOff) {
return ModeSwitchWidget(
onChange: (value) => _setOnOf(entity, value),
@ -232,7 +233,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
} else {
return Container(height: 0.0, width: 0.0,);
}
}
}*/
Widget _buildAuxHeatControl(ClimateEntity entity) {
if (entity.supportAuxHeat ) {
@ -247,12 +248,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
Widget _buildOperationControl(ClimateEntity entity) {
if (entity.supportOperationMode) {
if (entity.hvacModes != null) {
return ModeSelectorWidget(
onChange: (mode) => _setOperationMode(entity, mode),
options: entity.operationList,
onChange: (mode) => _setHVACMode(entity, mode),
options: entity.hvacModes,
caption: "Operation",
value: _tmpOperationMode,
value: _tmpHVACMode,
);
} else {
return Container(height: 0.0, width: 0.0);
@ -262,7 +263,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
Widget _buildFanControl(ClimateEntity entity) {
if (entity.supportFanMode) {
return ModeSelectorWidget(
options: entity.fanList,
options: entity.fanModes,
onChange: (mode) => _setFanMode(entity, mode),
caption: "Fan mode",
value: _tmpFanMode,
@ -276,7 +277,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
if (entity.supportSwingMode) {
return ModeSelectorWidget(
onChange: (mode) => _setSwingMode(entity, mode),
options: entity.swingList,
options: entity.swingModes,
value: _tmpSwingMode,
caption: "Swing mode"
);
@ -308,7 +309,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
List<Widget> controls = [];
if ((entity.supportTargetTemperatureLow) && (entity.targetLow != null)) {
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
controls.addAll(<Widget>[
TemperatureControlWidget(
value: _tmpTargetLow,
@ -321,7 +322,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
)
]);
}
if ((entity.supportTargetTemperatureHigh) && (entity.targetHigh != null)) {
if ((entity.supportTargetTemperatureRange) && (entity.targetHigh != null)) {
controls.add(
TemperatureControlWidget(
value: _tmpTargetHigh,
@ -407,53 +408,4 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
super.dispose();
}
}
class TemperatureControlWidget extends StatelessWidget {
final double value;
final double fontSize;
final Color fontColor;
final onInc;
final onDec;
TemperatureControlWidget(
{Key key,
@required this.value,
@required this.onInc,
@required this.onDec,
this.fontSize,
this.fontColor})
: super(key: key);
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
"$value",
style: TextStyle(
fontSize: fontSize ?? 24.0,
color: fontColor ?? Colors.black
),
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
'mdi:chevron-up')),
iconSize: 30.0,
onPressed: () => onInc(),
),
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
'mdi:chevron-down')),
iconSize: 30.0,
onPressed: () => onDec(),
)
],
)
],
);
}
}

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class ClimateStateWidget extends StatelessWidget {
@override
@ -8,13 +8,19 @@ class ClimateStateWidget extends StatelessWidget {
String targetTemp = "-";
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
targetTemp = "${entity.temperature}";
} else if ((entity.supportTargetTemperatureLow) &&
(entity.targetLow != null)) {
targetTemp = "${entity.targetLow}";
if ((entity.supportTargetTemperatureHigh) &&
(entity.targetHigh != null)) {
targetTemp += " - ${entity.targetHigh}";
}
} else if ((entity.supportTargetTemperatureRange) &&
(entity.targetLow != null) &&
(entity.targetHigh != null)) {
targetTemp = "${entity.targetLow} - ${entity.targetHigh}";
}
String displayState = '';
if (entity.hvacAction != null) {
displayState = "${entity.hvacAction} (${entity.displayState})";
} else {
displayState = "${entity.displayState}";
}
if (entity.presetMode != null) {
displayState += " - ${entity.presetMode}";
}
return Padding(
padding: EdgeInsets.fromLTRB(
@ -25,7 +31,7 @@ class ClimateStateWidget extends StatelessWidget {
children: <Widget>[
Row(
children: <Widget>[
Text("${entity.state}",
Text("$displayState",
textAlign: TextAlign.right,
style: new TextStyle(
fontWeight: FontWeight.bold,
@ -38,8 +44,8 @@ class ClimateStateWidget extends StatelessWidget {
))
],
),
entity.attributes["current_temperature"] != null ?
Text("Currently: ${entity.attributes["current_temperature"]}",
entity.currentTemperature != null ?
Text("Currently: ${entity.currentTemperature}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,

View File

@ -0,0 +1,50 @@
part of '../../../main.dart';
class TemperatureControlWidget extends StatelessWidget {
final double value;
final double fontSize;
final Color fontColor;
final onInc;
final onDec;
TemperatureControlWidget(
{Key key,
@required this.value,
@required this.onInc,
@required this.onDec,
this.fontSize,
this.fontColor})
: super(key: key);
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
"$value",
style: TextStyle(
fontSize: fontSize ?? 24.0,
color: fontColor ?? Colors.black
),
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
'mdi:chevron-up')),
iconSize: 30.0,
onPressed: () => onInc(),
),
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
'mdi:chevron-down')),
iconSize: 30.0,
onPressed: () => onDec(),
)
],
)
],
);
}
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class CoverEntity extends Entity {

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class CoverControlWidget extends StatefulWidget {

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class CoverStateWidget extends StatelessWidget {
void _open(CoverEntity entity) {

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class DateTimeEntity extends Entity {
DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class DateTimeStateWidget extends StatelessWidget {
@override

View File

@ -27,7 +27,7 @@ class Entity {
"cold.on": "Cold",
"cold.off": "Normal",
"connectivity.on": "Connected",
"connectivity.off": "Diconnected",
"connectivity.off": "Disconnected",
"door.on": "Open",
"door.off": "Closed",
"garage_door.on": "Open",
@ -154,7 +154,7 @@ class Entity {
entityId = rawData["entity_id"];
deviceClass = attributes["device_class"];
state = rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
entityPicture = _getEntityPictureUrl(webHost);
}
@ -216,7 +216,7 @@ class Entity {
entityWrapper: EntityWrapper(entity: this),
child: EntityPageContainer(children: <Widget>[
Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding),
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
),
LastUpdatedWidget(),

View File

@ -4,6 +4,7 @@ class EntityWrapper {
String displayName;
String icon;
String unitOfMeasurement;
String entityPicture;
EntityUIAction uiAction;
Entity entity;
@ -24,6 +25,7 @@ class EntityWrapper {
if (uiAction == null) {
uiAction = EntityUIAction();
}
unitOfMeasurement = entity.unitOfMeasurement;
}
}
@ -60,7 +62,7 @@ class EntityWrapper {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
HAUtils.launchURL(uiAction.tapService);
Launcher.launchURL(uiAction.tapService);
}
break;
}
@ -100,7 +102,7 @@ class EntityWrapper {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
HAUtils.launchURL(uiAction.holdService);
Launcher.launchURL(uiAction.holdService);
}
break;
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class FanEntity extends Entity {

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class FanControlsWidget extends StatefulWidget {

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class GroupEntity extends Entity {

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class LightEntity extends Entity {

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class LightControlsWidget extends StatefulWidget {
@ -17,7 +17,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
String _tmpEffect;
void _resetState(LightEntity entity) {
_tmpBrightness = entity.brightness ?? 0;
_tmpBrightness = entity.brightness ?? 1;
_tmpWhiteValue = entity.whiteValue ?? 0;
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
_tmpColor = entity.color ?? _tmpColor;
@ -28,15 +28,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() {
_tmpBrightness = value.round();
_changedHere = true;
if (_tmpBrightness > 0) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"brightness": _tmpBrightness}));
} else {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_off", entity.entityId,
null));
}
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"brightness": _tmpBrightness}));
});
}
@ -114,10 +108,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpBrightness = value.round();
});
},
min: 0.0,
min: 1.0,
max: 255.0,
onChangeEnd: (value) => _setBrightness(entity, value),
value: _tmpBrightness == null ? 0.0 : _tmpBrightness.toDouble(),
value: _tmpBrightness == null ? 1.0 : _tmpBrightness.toDouble(),
leading: Icon(Icons.brightness_5),
title: "Brightness",
);
@ -171,7 +165,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
Widget _buildColorControl(LightEntity entity) {
if (entity.supportColor) {
HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor;
HSVColor savedColor = HomeAssistant().savedColor;
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
@ -187,10 +181,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
child: Text('Copy color'),
onPressed: _tmpColor == null ? null : () {
setState(() {
HomeAssistantModel
.of(context)
.homeAssistant
.savedColor = _tmpColor;
HomeAssistant().savedColor = _tmpColor;
});
},
),

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class LockEntity extends Entity {
LockEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class LockStateWidget extends StatelessWidget {

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class MediaPlayerEntity extends Entity {

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class MediaPlayerWidget extends StatelessWidget {

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class SelectEntity extends Entity {
List<String> get listOptions => attributes["options"] != null

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class SelectStateWidget extends StatefulWidget {

View File

@ -1,8 +1,4 @@
part of '../main.dart';
class SunEntity extends Entity {
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
}
part of '../../main.dart';
class SensorEntity extends Entity {

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class SliderEntity extends Entity {
SliderEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class SliderControlsWidget extends StatefulWidget {

View File

@ -0,0 +1,5 @@
part of '../../main.dart';
class SunEntity extends Entity {
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class SwitchEntity extends Entity {
SwitchEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class SwitchStateWidget extends StatefulWidget {

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class TextEntity extends Entity {
TextEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class TextInputStateWidget extends StatefulWidget {
@ -73,13 +73,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
child: TextField(
focusNode: _focusNode,
obscureText: entity.isPasswordField,
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _tmpValue,
selection:
new TextSelection.collapsed(offset: _tmpValue.length)
)
),
controller: TextEditingController.fromValue(TextEditingValue(text: _tmpValue)),
onChanged: (value) {
_tmpValue = value;
}),

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class TimerEntity extends Entity {
TimerEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class TimerState extends StatefulWidget {
//final bool expanded;

View File

@ -149,6 +149,17 @@ class EntityCollection {
return _allEntities[entityId] != null;
}
List<Entity> getByDomains(List<String> domains) {
List<Entity> result = [];
_allEntities.forEach((id, entity) {
if (domains.contains(entity.domain)) {
Logger.d("getByDomain: ${entity.isHidden}");
result.add(entity);
}
});
return result;
}
List<Entity> filterEntitiesForDefaultView() {
List<Entity> result = [];
List<Entity> groups = [];

View File

@ -35,25 +35,36 @@ class BadgeWidget extends StatelessWidget {
break;
}
case "device_tracker":
case "person":
{
badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
);
onBadgeTextValue = entityModel.entityWrapper.entity.state;
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
break;
}
default:
{
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
double stateFontSize;
if (entityModel.entityWrapper.entity.displayState.length <= 3) {
stateFontSize = 18.0;
} else if (entityModel.entityWrapper.entity.displayState.length <= 4) {
stateFontSize = 15.0;
} else if (entityModel.entityWrapper.entity.displayState.length <= 6) {
stateFontSize = 10.0;
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
stateFontSize = 8.0;
}
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
badgeIcon = Center(
child: Text(
"${entityModel.entityWrapper.entity.state}",
"${entityModel.entityWrapper.entity.displayState}",
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 17.0),
style: TextStyle(fontSize: stateFontSize),
),
);
break;

View File

@ -16,112 +16,14 @@ class _CameraStreamViewState extends State<CameraStreamView> {
}
CameraEntity _entity;
String _webHost;
http.Client client;
http.StreamedResponse response;
List<int> binaryImage = [];
bool timeToStop = false;
Completer streamCompleter;
bool started = false;
bool useSVG = false;
String streamUrl = "";
void _connect() async {
started = true;
timeToStop = false;
String streamUrl = '$_webHost/api/camera_proxy_stream/${_entity.entityId}?token=${_entity.attributes['access_token']}';
client = new http.Client(); // create a client to make api calls
http.Request request = new http.Request("GET", Uri.parse(streamUrl)); // create get request
Logger.d("[Sending] ==> $streamUrl");
response = await client.send(request);
Logger.d("[Received] <== ${response.headers}");
String frameBoundary = response.headers['content-type'].split('boundary=')[1];
final int frameBoundarySize = frameBoundary.length;
List<int> primaryBuffer=[];
int imageSizeStart = 59;
int imageSizeEnd = 0;
int imageStart = 0;
int imageSize = 0;
String strBuffer = "";
String contentType = "";
streamCompleter = Completer();
response.stream.transform(
StreamTransformer.fromHandlers(
handleData: (data, sink) {
primaryBuffer.addAll(data);
imageStart = 0;
imageSizeEnd = 0;
if (primaryBuffer.length >= imageSizeStart + 10) {
contentType = utf8.decode(
primaryBuffer.sublist(frameBoundarySize+16, imageSizeStart + 10), allowMalformed: true).split("\r\n")[0];
useSVG = contentType == "image/svg+xml";
imageSizeStart = frameBoundarySize + 16 + contentType.length + 18;
for (int i = imageSizeStart; i < primaryBuffer.length - 4; i++) {
strBuffer = utf8.decode(
primaryBuffer.sublist(i, i + 4), allowMalformed: true);
if (strBuffer == "\r\n\r\n") {
imageSizeEnd = i;
imageStart = i + 4;
break;
}
}
if (imageSizeEnd > 0) {
imageSize = int.tryParse(utf8.decode(
primaryBuffer.sublist(imageSizeStart, imageSizeEnd),
allowMalformed: true));
//Logger.d("content-length: $imageSize");
if (imageSize != null &&
primaryBuffer.length >= imageStart + imageSize + 2) {
sink.add(
primaryBuffer.sublist(
imageStart, imageStart + imageSize));
primaryBuffer.removeRange(0, imageStart + imageSize + 2);
}
}
}
if (timeToStop) {
sink?.close();
streamCompleter.complete();
}
},
handleError: (error, stack, sink) {
Logger.e("Error parsing MJPEG stream: $error");
},
handleDone: (sink) {
Logger.d("Camera stream finished. Reconnecting...");
sink?.close();
streamCompleter?.complete();
_reconnect();
},
)
).listen((d) {
if (!timeToStop) {
setState(() {
binaryImage = d;
});
}
});
}
void _reconnect() {
disconnect().then((_){
_connect();
});
}
Future disconnect() {
Completer disconF = Completer();
timeToStop = true;
if (streamCompleter != null && !streamCompleter.isCompleted) {
streamCompleter.future.then((_) {
client?.close();
disconF.complete();
});
} else {
client?.close();
disconF.complete();
}
return disconF.future;
launchStream() {
Launcher.launchURLInCustomTab(
context: context,
url: streamUrl
);
}
@override
@ -131,47 +33,26 @@ class _CameraStreamViewState extends State<CameraStreamView> {
.of(context)
.entityWrapper
.entity;
_webHost = HomeAssistantModel.of(context).homeAssistant.connection.httpWebHost;
_connect();
started = true;
}
if (binaryImage.isEmpty) {
return Column(
children: <Widget>[
Container(
padding: const EdgeInsets.all(20.0),
child: const CircularProgressIndicator()
)
],
);
} else {
if (useSVG) {
return Column(
children: <Widget>[
SvgPicture.memory(
Uint8List.fromList(binaryImage),
placeholderBuilder: (BuildContext context) =>
new Container(
padding: const EdgeInsets.all(20.0),
child: const CircularProgressIndicator()
),
streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
.entityId}?token=${_entity.attributes['access_token']}';
return Column(
children: <Widget>[
Container(
padding: const EdgeInsets.all(20.0),
child: IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
iconSize: 50.0,
onPressed: () => launchStream(),
)
],
);
} else {
return Column(
children: <Widget>[
Image.memory(
Uint8List.fromList(binaryImage), gaplessPlayback: true),
],
);
}
}
)
],
);
}
@override
void dispose() {
disconnect();
super.dispose();
}
}

View File

@ -7,8 +7,10 @@ class SimpleEntityState extends StatelessWidget {
final EdgeInsetsGeometry padding;
final int maxLines;
final String customValue;
final double fontSize;
final bool bold;
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
const SimpleEntityState({Key key,this.bold: false, this.maxLines: 10, this.fontSize: Sizes.stateFontSize, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -21,18 +23,22 @@ class SimpleEntityState extends StatelessWidget {
state = customValue;
}
TextStyle textStyle = TextStyle(
fontSize: Sizes.stateFontSize,
fontSize: this.fontSize,
fontWeight: FontWeight.normal
);
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
textStyle = textStyle.apply(color: Colors.blue);
}
if (this.bold) {
textStyle = textStyle.apply(fontWeightDelta: 100);
}
while (state.contains(" ")){
state = state.replaceAll(" ", " ");
}
Widget result = Padding(
padding: padding,
child: Text(
"$state ${entityModel.entityWrapper.entity.unitOfMeasurement}",
"$state ${entityModel.entityWrapper.unitOfMeasurement}",
textAlign: textAlign,
maxLines: maxLines,
overflow: TextOverflow.ellipsis,

View File

@ -15,14 +15,18 @@ class DefaultEntityContainer extends StatelessWidget {
return MissedEntityWidget();
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
return Divider();
return Divider(
color: Colors.black45,
);
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Divider(),
Divider(
color: Colors.black45,
),
Text(
"${entityModel.entityWrapper.entity.displayName}",
style: TextStyle(color: Colors.blue),
@ -30,32 +34,37 @@ class DefaultEntityContainer extends StatelessWidget {
],
);
}
return InkWell(
onLongPress: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleHold();
}
},
onTap: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleTap();
}
},
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
EntityIcon(),
Widget result = Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
EntityIcon(),
Flexible(
fit: FlexFit.tight,
flex: 3,
child: EntityName(
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
),
Flexible(
fit: FlexFit.tight,
flex: 3,
child: EntityName(
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
),
state
],
),
),
state
],
);
if (entityModel.handleTap) {
return InkWell(
onLongPress: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleHold();
}
},
onTap: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleTap();
}
},
child: result,
);
} else {
return result;
}
}
}

View File

@ -14,6 +14,7 @@ class EntityColor {
"auto": Colors.amber,
EntityState.active: Colors.amber,
EntityState.playing: Colors.amber,
EntityState.paused: Colors.amber,
"above_horizon": Colors.amber,
EntityState.home: Colors.amber,
EntityState.open: Colors.amber,

View File

@ -148,7 +148,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
});
if ((_selectedId == -1) && (numericDataLists.isNotEmpty)) {
_selectedId = 0;
_selectedId = numericDataLists.length -1;
}
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
numericDataLists.forEach((attrName, dataList) {
@ -202,6 +202,11 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
_selectedId -= 1;
});
}
else {
setState(() {
_selectedId = _parsedHistory.first.data.length - 1;
});
}
}
void _selectNext() {
@ -210,6 +215,12 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
_selectedId += 1;
});
}
else {
setState(() {
_selectedId = 0;
});
}
}
void _onSelectionChanged(charts.SelectionModel model) {

View File

@ -40,14 +40,14 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
_needToUpdateHistory = true;
}
void _loadHistory(HomeAssistant ha, String entityId) {
void _loadHistory(String entityId) {
DateTime now = DateTime.now();
if (_historyLastUpdated != null) {
Logger.d("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
}
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
_historyLastUpdated = now;
ha.connection.getHistory(entityId).then((history){
ConnectionManager().getHistory(entityId).then((history){
if (!_disposed) {
setState(() {
_history = history.isNotEmpty ? history[0] : [];
@ -68,13 +68,12 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
@override
Widget build(BuildContext context) {
final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context);
final EntityModel entityModel = EntityModel.of(context);
final Entity entity = entityModel.entityWrapper.entity;
if (!_needToUpdateHistory) {
_needToUpdateHistory = true;
} else {
_loadHistory(homeAssistantModel.homeAssistant, entity.entityId);
_loadHistory(entity.entityId);
}
return _buildChart();
}

View File

@ -103,7 +103,7 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
id: widget.rawHistory.length
));
if (_selectedId == -1) {
_selectedId = 0;
_selectedId = data.length - 1;
}
return [
new charts.Series<EntityHistoryMoment, DateTime>(
@ -132,6 +132,11 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
_selectedId -= 1;
});
}
else {
setState(() {
_selectedId = _parsedHistory.first.data.length - 1;
});
}
}
void _selectNext() {
@ -140,6 +145,12 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
_selectedId += 1;
});
}
else {
setState(() {
_selectedId = 0;
});
}
}
void _onSelectionChanged(charts.SelectionModel model) {

View File

@ -101,7 +101,7 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
colorId: data.last.colorId
));
if (_selectedId == -1) {
_selectedId = 0;
_selectedId = data.length - 1;
}
return [
new charts.Series<EntityHistoryMoment, DateTime>(
@ -137,14 +137,25 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
_selectedId -= 1;
});
}
else {
setState(() {
_selectedId = _parsedHistory.first.data.length - 1;
});
}
}
void _selectNext() {
if (_selectedId < (_parsedHistory.first.data.length - 2)) {
if (_selectedId < (_parsedHistory.first.data.length - 1)) {
setState(() {
_selectedId += 1;
});
}
else {
setState(() {
_selectedId = 0;
});
}
}
void _onSelectionChanged(charts.SelectionModel model) {

View File

@ -15,26 +15,6 @@ class EntityModel extends InheritedWidget {
return context.inheritFromWidgetOfExactType(EntityModel);
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
}
}
class HomeAssistantModel extends InheritedWidget {
const HomeAssistantModel({
Key key,
@required this.homeAssistant,
@required Widget child,
}) : super(key: key, child: child);
final HomeAssistant homeAssistant;
static HomeAssistantModel of(BuildContext context) {
return context.inheritFromWidgetOfExactType(HomeAssistantModel);
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;

View File

@ -2,21 +2,21 @@ part of 'main.dart';
class HomeAssistant {
final Connection connection = Connection();
bool _useLovelace = false;
//bool isSettingsLoaded = false;
static final HomeAssistant _instance = HomeAssistant._internal();
factory HomeAssistant() {
return _instance;
}
EntityCollection entities;
HomeAssistantUI ui;
Map _instanceConfig = {};
Map services;
String _userName;
String hostname;
HSVColor savedColor;
String fcmToken;
Map _rawLovelaceData;
List<Panel> panels = [];
@ -24,7 +24,7 @@ class HomeAssistant {
Duration fetchTimeout = Duration(seconds: 30);
String get locationName {
if (_useLovelace) {
if (ConnectionManager().useLovelace) {
return ui?.title ?? "";
} else {
return _instanceConfig["location_name"] ?? "";
@ -34,49 +34,43 @@ class HomeAssistant {
String get userAvatarText => userName.length > 0 ? userName[0] : "";
bool get isNoEntities => entities == null || entities.isEmpty;
bool get isNoViews => ui == null || ui.isEmpty;
//int get viewsCount => entities.views.length ?? 0;
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
HomeAssistant();
Completer _connectCompleter;
Future init() {
if (_connectCompleter != null && !_connectCompleter.isCompleted) {
Logger.w("Previous connection pending...");
return _connectCompleter.future;
}
Logger.d("init...");
_connectCompleter = Completer();
connection.init(_handleEntityStateChange).then((_) {
SharedPreferences.getInstance().then((prefs) {
if (entities == null) entities = EntityCollection(connection.httpWebHost);
_useLovelace = prefs.getBool('use-lovelace') ?? true;
_connectCompleter.complete();
}).catchError((e) => _connectCompleter.completeError(e));
}).catchError((e) => _connectCompleter.completeError(e));
return _connectCompleter.future;
HomeAssistant._internal() {
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
DeviceInfoManager().loadDeviceInfo();
}
Completer _fetchCompleter;
Future fetch() {
Future fetchData() {
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
Logger.w("Previous data fetch is not completed yet");
return _fetchCompleter.future;
}
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
_fetchCompleter = Completer();
List<Future> futures = [];
futures.add(_getStates());
if (_useLovelace) {
if (ConnectionManager().useLovelace) {
futures.add(_getLovelace());
}
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((_) {
_createUI();
_fetchCompleter.complete();
if (isMobileAppEnabled) {
_createUI();
_fetchCompleter.complete();
MobileAppIntegrationManager.checkAppRegistration();
} else {
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app-integration")]));
}
}).catchError((e) {
_fetchCompleter.completeError(e);
});
@ -85,50 +79,55 @@ class HomeAssistant {
Future logout() async {
Logger.d("Logging out...");
await connection.logout().then((_) {
await ConnectionManager().logout().then((_) {
ui?.clear();
entities?.clear();
panels?.clear();
});
}
Future _getConfig() async {
await connection.sendSocketMessage(type: "get_config").then((data) {
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
_instanceConfig = Map.from(data);
}).catchError((e) {
throw {"errorCode": 1, "errorMessage": "Error getting config: $e"};
throw HAError("Error getting config: ${e}");
});
}
Future _getStates() async {
await connection.sendSocketMessage(type: "get_states").then(
await ConnectionManager().sendSocketMessage(type: "get_states").then(
(data) => entities.parse(data)
).catchError((e) {
throw {"errorCode": 1, "errorMessage": "Error getting states: $e"};
throw HAError("Error getting states: $e");
});
}
Future _getLovelace() async {
await connection.sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
throw {"errorCode": 1, "errorMessage": "Error getting lovelace config: $e"};
await ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
throw HAError("Error getting lovelace config: $e");
});
}
Future _getUserInfo() async {
_userName = null;
await connection.sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
Logger.w("Can't get user info: ${e}");
});
}
Future _getServices() async {
await connection.sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) {
Logger.d("Got ${data.length} services");
Logger.d("Media extractor: ${data["media_extractor"]}");
services = data;
}).catchError((e) {
Logger.w("Can't get services: ${e}");
});
}
Future _getPanels() async {
panels.clear();
await connection.sendSocketMessage(type: "get_panels").then((data) {
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) {
data.forEach((k,v) {
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
panels.add(Panel(
@ -142,17 +141,19 @@ class HomeAssistant {
);
});
}).catchError((e) {
throw {"errorCode": 1, "errorMessage": "Error getting panels list: $e"};
throw HAError("Error getting panels list: $e");
});
}
void _handleEntityStateChange(Map eventData) {
//TheLogger.debug( "New state for ${eventData['entity_id']}");
Map data = Map.from(eventData);
eventBus.fire(new StateChangedEvent(
entityId: data["entity_id"],
needToRebuildUI: entities.updateState(data)
));
if (_fetchCompleter.isCompleted) {
Map data = Map.from(eventData);
eventBus.fire(new StateChangedEvent(
entityId: data["entity_id"],
needToRebuildUI: entities.updateState(data)
));
}
}
void _parseLovelace() {
@ -166,7 +167,8 @@ class HomeAssistant {
count: viewCounter,
id: "${rawView['id']}",
name: rawView['title'],
iconName: rawView['icon']
iconName: rawView['icon'],
panel: rawView['panel'] ?? false,
);
if (rawView['badges'] != null && rawView['badges'] is List) {
@ -190,31 +192,29 @@ class HomeAssistant {
List<HACard> result = [];
rawCards.forEach((rawCard){
try {
bool isThereCardOptionsInside = rawCard["card"] != null;
//bool isThereCardOptionsInside = rawCard["card"] != null;
var rawCardInfo = rawCard["card"] ?? rawCard;
HACard card = HACard(
id: "card",
name: isThereCardOptionsInside ? rawCard["card"]["title"] ??
rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"],
type: isThereCardOptionsInside
? rawCard["card"]['type']
: rawCard['type'],
columnsCount: isThereCardOptionsInside
? rawCard["card"]['columns'] ?? 4
: rawCard['columns'] ?? 4,
showName: isThereCardOptionsInside ? rawCard["card"]['show_name'] ??
true : rawCard['show_name'] ?? true,
showState: isThereCardOptionsInside
? rawCard["card"]['show_state'] ?? true
: rawCard['show_state'] ?? true,
showEmpty: rawCard['show_empty'] ?? true,
stateFilter: rawCard['state_filter'] ?? [],
states: rawCard['states'],
content: rawCard['content']
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 (rawCard["cards"] != null) {
card.childCards = _createLovelaceCards(rawCard["cards"]);
if (rawCardInfo["cards"] != null) {
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
}
rawCard["entities"]?.forEach((rawEntity) {
rawCardInfo["entities"]?.forEach((rawEntity) {
if (rawEntity is String) {
if (entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
@ -277,15 +277,15 @@ class HomeAssistant {
}
}
});
if (rawCard["entity"] != null) {
var en = rawCard["entity"];
if (rawCardInfo["entity"] != null) {
var en = rawCardInfo["entity"];
if (en is String) {
if (entities.isExist(en)) {
Entity e = entities.get(en);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
icon: rawCard["icon"],
displayName: rawCard["name"],
icon: rawCardInfo["icon"],
displayName: rawCardInfo["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
@ -315,7 +315,7 @@ class HomeAssistant {
void _createUI() {
ui = HomeAssistantUI();
if ((_useLovelace) && (_rawLovelaceData != null)) {
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
Logger.d("Creating Lovelace UI");
_parseLovelace();
} else {

View File

@ -1,6 +1,6 @@
import 'dart:convert';
import 'dart:async';
import 'dart:typed_data';
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -15,39 +15,48 @@ import 'package:http/http.dart' as http;
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:progress_indicators/progress_indicators.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:device_info/device_info.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'plugins/circular_slider/single_circular_slider.dart';
import 'package:share/receive_share_state.dart';
import 'package:share/share.dart';
part 'entity_class/const.dart';
part 'entity_class/entity.class.dart';
part 'entity_class/entity_wrapper.class.dart';
part 'entity_class/timer_entity.dart';
part 'entity_class/switch_entity.class.dart';
part 'entity_class/button_entity.class.dart';
part 'entity_class/text_entity.class.dart';
part 'entity_class/climate_entity.class.dart';
part 'entity_class/cover_entity.class.dart';
part 'entity_class/date_time_entity.class.dart';
part 'entity_class/light_entity.class.dart';
part 'entity_class/select_entity.class.dart';
part 'entity_class/other_entity.class.dart';
part 'entity_class/slider_entity.dart';
part 'entity_class/media_player_entity.class.dart';
part 'entity_class/lock_entity.class.dart';
part 'entity_class/group_entity.class.dart';
part 'entity_class/fan_entity.class.dart';
part 'entity_class/automation_entity.dart';
part 'entity_class/camera_entity.class.dart';
part 'entity_class/alarm_control_panel.class.dart';
import 'utils/logger.dart';
part 'const.dart';
part 'utils/launcher.dart';
part 'entities/entity.class.dart';
part 'entities/entity_wrapper.class.dart';
part 'entities/timer/timer_entity.class.dart';
part 'entities/switch/switch_entity.class.dart';
part 'entities/button/button_entity.class.dart';
part 'entities/text/text_entity.class.dart';
part 'entities/climate/climate_entity.class.dart';
part 'entities/cover/cover_entity.class.dart';
part 'entities/date_time/date_time_entity.class.dart';
part 'entities/light/light_entity.class.dart';
part 'entities/select/select_entity.class.dart';
part 'entities/sun/sun_entity.class.dart';
part 'entities/sensor/sensor_entity.class.dart';
part 'entities/slider/slider_entity.dart';
part 'entities/media_player/media_player_entity.class.dart';
part 'entities/lock/lock_entity.class.dart';
part 'entities/group/group_entity.class.dart';
part 'entities/fan/fan_entity.class.dart';
part 'entities/automation/automation_entity.class.dart';
part 'entities/camera/camera_entity.class.dart';
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
part 'entity_widgets/common/badge.dart';
part 'entity_widgets/model_widgets.dart';
part 'entity_widgets/default_entity_container.dart';
part 'entity_widgets/missed_entity.dart';
part 'entity_widgets/glance_entity_container.dart';
part 'entity_widgets/button_entity_container.dart';
part 'cards/widgets/glance_card_entity_container.dart';
part 'cards/widgets/entity_button_card_body.widget.dart';
part 'entity_widgets/common/entity_attributes_list.dart';
part 'entity_widgets/entity_icon.dart';
part 'entity_widgets/entity_name.dart';
@ -66,48 +75,64 @@ part 'entity_widgets/history_chart/numeric_state_history_chart.dart';
part 'entity_widgets/history_chart/combined_history_chart.dart';
part 'entity_widgets/history_chart/history_control_widget.dart';
part 'entity_widgets/history_chart/entity_history_moment.dart';
part 'entity_widgets/state/switch_state.dart';
part 'entity_widgets/controls/slider_controls.dart';
part 'entity_widgets/state/text_input_state.dart';
part 'entity_widgets/state/select_state.dart';
part 'entity_widgets/state/simple_state.dart';
part 'entity_widgets/state/timer_state.dart';
part 'entity_widgets/state/climate_state.dart';
part 'entity_widgets/state/cover_state.dart';
part 'entity_widgets/state/date_time_state.dart';
part 'entity_widgets/state/lock_state.dart';
part 'entity_widgets/controls/climate_controls.dart';
part 'entity_widgets/controls/cover_controls.dart';
part 'entity_widgets/controls/light_controls.dart';
part 'entity_widgets/controls/media_player_widgets.dart';
part 'entity_widgets/controls/fan_controls.dart';
part 'entity_widgets/controls/alarm_control_panel_controls.dart';
part 'settings.page.dart';
part 'panel.page.dart';
part 'entities/switch/widget/switch_state.dart';
part 'entities/slider/widgets/slider_controls.dart';
part 'entities/text/widgets/text_input_state.dart';
part 'entities/select/widgets/select_state.dart';
part 'entity_widgets/common/simple_state.dart';
part 'entities/timer/widgets/timer_state.dart';
part 'entities/climate/widgets/climate_state.widget.dart';
part 'entities/cover/widgets/cover_state.dart';
part 'entities/date_time/widgets/date_time_state.dart';
part 'entities/lock/widgets/lock_state.dart';
part 'entities/climate/widgets/climate_controls.dart';
part 'entities/climate/widgets/temperature_control_widget.dart';
part 'entities/cover/widgets/cover_controls.widget.dart';
part 'entities/light/widgets/light_controls.dart';
part 'entities/media_player/widgets/media_player_widgets.dart';
part 'entities/fan/widgets/fan_controls.dart';
part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.dart';
part 'pages/settings.page.dart';
part 'pages/purchase.page.dart';
part 'pages/widgets/product_purchase.widget.dart';
part 'pages/widgets/page_loading_indicator.dart';
part 'pages/widgets/page_loading_error.dart';
part 'pages/panel.page.dart';
part 'pages/main.page.dart';
part 'home_assistant.class.dart';
part 'log.page.dart';
part 'entity.page.dart';
part 'utils.class.dart';
part 'pages/log.page.dart';
part 'pages/entity.page.dart';
part 'mdi.class.dart';
part 'entity_collection.class.dart';
part 'auth_manager.class.dart';
part 'connection.class.dart';
part 'ui_class/ui.dart';
part 'ui_class/view.class.dart';
part 'ui_class/card.class.dart';
part 'ui_class/sizes_class.dart';
part 'ui_class/panel_class.dart';
part 'ui_widgets/view.dart';
part 'ui_widgets/card_widget.dart';
part 'ui_widgets/card_header_widget.dart';
part 'ui_widgets/config_panel_widget.dart';
part 'managers/auth_manager.class.dart';
part 'managers/location_manager.class.dart';
part 'managers/mobile_app_integration_manager.class.dart';
part 'managers/connection_manager.class.dart';
part 'managers/device_info_manager.class.dart';
part 'managers/startup_user_messages_manager.class.dart';
part 'ui.dart';
part 'view.class.dart';
part 'cards/card.class.dart';
part 'panels/panel_class.dart';
part 'view.dart';
part 'cards/card_widget.dart';
part 'cards/widgets/card_header.widget.dart';
part 'panels/config_panel_widget.dart';
part 'panels/widgets/link_to_web_config.dart';
part 'types/ha_error.dart';
part 'types/event_bus_events.dart';
part 'cards/widgets/gauge_card_body.dart';
part 'cards/widgets/light_card_body.dart';
part 'pages/play_media.page.dart';
EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = "HA Client";
const appVersion = "0.6.0-alpha1";
const appVersion = "0.6.6";
void main() {
void main() async {
FlutterError.onError = (errorDetails) {
Logger.e( "${errorDetails.exception}");
if (Logger.isInDebugMode) {
@ -116,7 +141,11 @@ void main() {
};
runZoned(() {
runApp(new HAClientApp());
//AndroidAlarmManager.initialize().then((_) {
runApp(new HAClientApp());
// print("Running MAIN isolate ${Isolate.current.hashCode}");
//});
}, onError: (error, stack) {
Logger.e("$error");
Logger.e("$stack");
@ -128,7 +157,6 @@ void main() {
class HAClientApp extends StatelessWidget {
final HomeAssistant homeAssistant = HomeAssistant();
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
@ -139,628 +167,40 @@ class HAClientApp extends StatelessWidget {
),
initialRoute: "/",
routes: {
"/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,),
"/": (context) => MainPage(title: 'HA Client'),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
"/configuration": (context) => PanelPage(title: "Configuration"),
"/log-view": (context) => LogViewPage(title: "Log")
},
);
}
}
class MainPage extends StatefulWidget {
MainPage({Key key, this.title, this.homeAssistant}) : super(key: key);
final String title;
final HomeAssistant homeAssistant;
@override
_MainPageState createState() => new _MainPageState();
}
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
StreamSubscription _stateSubscription;
StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription;
StreamSubscription _showErrorSubscription;
StreamSubscription _startAuthSubscription;
StreamSubscription _reloadUISubscription;
int _previousViewCount;
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
@override
void initState() {
super.initState();
//widget.homeAssistant = HomeAssistant();
//_settingsLoaded = false;
WidgetsBinding.instance.addObserver(this);
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
Logger.d("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) {
_reLoad();
}
});
_initialLoad();
}
void _initialLoad() {
_showInfoBottomBar(progress: true,);
_subscribe();
widget.homeAssistant.init().then((_){
_fetchData();
}, onError: (e) {
_setErrorState(e);
});
}
void _reLoad() {
_hideBottomBar();
_showInfoBottomBar(progress: true,);
widget.homeAssistant.init().then((_){
_fetchData();
}, onError: (e) {
_setErrorState(e);
});
}
_fetchData() async {
await widget.homeAssistant.fetch().then((_) {
_hideBottomBar();
int currentViewCount = widget.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;
}
}).catchError((e) {
_setErrorState(e);
});
eventBus.fire(RefreshDataFinishedEvent());
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
Logger.d("$state");
if (state == AppLifecycleState.resumed) {
_reLoad();
}
}
_subscribe() {
if (_stateSubscription == null) {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) {
Logger.d("New entity. Need to rebuild UI");
_reLoad();
} else {
setState(() {});
}
});
}
if (_reloadUISubscription == null) {
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
_reLoad();
});
}
if (_serviceCallSubscription == null) {
_serviceCallSubscription =
eventBus.on<ServiceCallEvent>().listen((event) {
_callService(event.domain, event.service, event.entityId,
event.additionalParams);
});
}
if (_showEntityPageSubscription == null) {
_showEntityPageSubscription =
eventBus.on<ShowEntityPageEvent>().listen((event) {
_showEntityPage(event.entity.entityId);
});
}
if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_showErrorBottomBar(message: event.text, errorCode: event.errorCode);
});
}
if (_startAuthSubscription == null) {
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
_showOAuth();
});
}
/*_firebaseMessaging.getToken().then((String token) {
//Logger.d("FCM token: $token");
widget.homeAssistant.sendHTTPPost(
endPoint: '/api/notify.fcm-android',
jsonData: '{"token": "$token"}'
);
});
_firebaseMessaging.configure(
onLaunch: (data) {
Logger.d("Notification [onLaunch]: $data");
},
onMessage: (data) {
Logger.d("Notification [onMessage]: $data");
},
onResume: (data) {
Logger.d("Notification [onResume]: $data");
}
);*/
}
void _showOAuth() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebviewScaffold(
url: "${widget.homeAssistant.connection.oauthUrl}",
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.help),
onPressed: () => HAUtils.launchURLInCustomTab(context, "http://ha-client.homemade.systems/docs#authentication")
),
title: new Text("Login to your Home Assistant"),
"/putchase": (context) => PurchasePage(title: "Support app development"),
"/play-media": (context) => PlayMediaPage(mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",),
"/log-view": (context) => LogViewPage(title: "Log"),
"/login": (context) => WebviewScaffold(
url: "${ConnectionManager().oauthUrl}",
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.help),
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#authentication")
),
),
)
);
}
_setErrorState(e) {
if (e is Error) {
Logger.e(e.toString());
Logger.e("${e.stackTrace}");
_showErrorBottomBar(
message: "Unknown error",
errorCode: 13
);
} else {
_showErrorBottomBar(
message: e != null ? e["errorMessage"] ?? "$e" : "Unknown error",
errorCode: e["errorCode"] != null ? e["errorCode"] : 99
);
}
}
void _callService(String domain, String service, String entityId, Map additionalParams) {
_showInfoBottomBar(
message: "Calling $domain.$service",
duration: Duration(seconds: 3)
);
widget.homeAssistant.connection.callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
}
void _showEntityPage(String entityId) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: widget.homeAssistant),
)
);
}
List<Tab> buildUIViewTabs() {
List<Tab> result = [];
if (widget.homeAssistant.ui.views.isNotEmpty) {
widget.homeAssistant.ui.views.forEach((HAView view) {
result.add(view.buildTab());
});
}
return result;
}
Drawer _buildAppDrawer() {
List<Widget> menuItems = [];
menuItems.add(
UserAccountsDrawerHeader(
accountName: Text(widget.homeAssistant.userName),
accountEmail: Text(widget.homeAssistant.hostname ?? "Not configured"),
/*onDetailsPressed: () {
setState(() {
_accountMenuExpanded = !_accountMenuExpanded;
});
},*/
currentAccountPicture: CircleAvatar(
child: Text(
widget.homeAssistant.userAvatarText,
style: TextStyle(
fontSize: 32.0
),
),
),
)
);
if (widget.homeAssistant.panels.isNotEmpty) {
widget.homeAssistant.panels.forEach((Panel panel) {
if (!panel.isHidden) {
menuItems.add(
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
title: Text("${panel.title}"),
onTap: () => panel.handleOpen(context)
)
);
}
});
}
//TODO check for loaded
menuItems.add(
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
title: Text("Open Web UI"),
onTap: () => HAUtils.launchURL(widget.homeAssistant.connection.httpWebHost),
)
);
menuItems.addAll([
Divider(),
ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
title: Text("Connection settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/connection-settings', arguments: {"homeAssistant", widget.homeAssistant});
},
)
]);
menuItems.addAll([
Divider(),
new ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text("Log"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/log-view');
},
),
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
title: Text("Report an issue"),
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
},
),
Divider(),
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
title: Text("Join Discord server"),
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURL("https://discord.gg/AUzEvwn");
},
),
new AboutListTile(
aboutBoxChildren: <Widget>[
GestureDetector(
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURL("http://ha-client.homemade.systems/");
},
child: Text(
"ha-client.homemade.systems",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
),
),
)
],
applicationName: appName,
applicationVersion: appVersion
)
]);
return new Drawer(
child: ListView(
children: menuItems,
),
);
}
void _hideBottomBar() {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
setState(() {
_showBottomBar = false;
});
}
Widget _bottomBarAction;
bool _showBottomBar = false;
String _bottomBarText;
bool _bottomBarProgress;
Color _bottomBarColor;
Timer _bottomBarTimer;
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
_bottomBarTimer?.cancel();
_bottomBarAction = Container(height: 0.0, width: 0.0,);
_bottomBarColor = Colors.grey.shade50;
setState(() {
_bottomBarText = message;
_bottomBarProgress = progress;
_showBottomBar = true;
});
if (duration != null) {
_bottomBarTimer = Timer(duration, () {
_hideBottomBar();
});
}
}
void _showErrorBottomBar({Key key, @required String message, @required int errorCode}) {
TextStyle textStyle = TextStyle(
color: Colors.blue,
fontSize: Sizes.nameFontSize
);
_bottomBarColor = Colors.red.shade100;
switch (errorCode) {
case 9:
case 11:
case 7:
case 1: {
_bottomBarAction = FlatButton(
child: Text("Retry", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_reLoad();
},
);
break;
}
case 5: {
message = "Check connection settings";
_bottomBarAction = FlatButton(
child: Text("Open", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
Navigator.pushNamed(context, '/connection-settings');
},
);
break;
}
case 60: {
_bottomBarAction = FlatButton(
child: Text("Login", style: textStyle),
onPressed: () {
_reLoad();
},
);
break;
}
case 63:
case 61: {
_bottomBarAction = FlatButton(
child: Text("Try again", style: textStyle),
onPressed: () {
_reLoad();
},
);
break;
}
case 62: {
_bottomBarAction = FlatButton(
child: Text("Login again", style: textStyle),
onPressed: () {
_reLoad();
},
);
break;
}
case 10: {
_bottomBarAction = FlatButton(
child: Text("Refresh", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_reLoad();
},
);
break;
}
case 82:
case 81:
case 8: {
_bottomBarAction = FlatButton(
child: Text("Reconnect", style: textStyle),
onPressed: () {
_reLoad();
},
);
break;
}
default: {
_bottomBarAction = Container(width: 0.0, height: 0.0,);
break;
}
}
setState(() {
_bottomBarProgress = false;
_bottomBarText = "$message";
_showBottomBar = true;
});
}
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> popupMenuItems = [];
popupMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
));
if (widget.homeAssistant.connection.isAuthenticated) {
popupMenuItems.add(
PopupMenuItem<String>(
child: new Text("Logout"),
value: "logout",
));
}
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
floating: true,
pinned: true,
primary: true,
title: Text(widget.homeAssistant.locationName ?? ""),
title: new Text("Login with HA"),
actions: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Colors.white,),
FlatButton(
child: Text("Manual", style: TextStyle(color: Colors.white)),
onPressed: () {
showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
context: context,
items: popupMenuItems
).then((String val) {
if (val == "reload") {
_reLoad();
} else if (val == "logout") {
widget.homeAssistant.logout().then((_) {
_reLoad();
});
}
});
}
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
},
)
],
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
},
),
bottom: empty ? null : TabBar(
controller: _viewsTabController,
tabs: buildUIViewTabs(),
isScrollable: true,
),
),
];
},
body: empty ?
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
MaterialDesignIcons.getIconDataFromIconName("mdi:border-none-variant"),
size: 100.0,
color: Colors.black26,
),
]
),
)
:
widget.homeAssistant.buildViews(context, _viewsTabController),
"/webview": (context) => WebviewScaffold(
url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}",
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop()
),
title: new Text("${(ModalRoute.of(context).settings.arguments as Map)['title']}"),
),
)
},
);
}
TabController _viewsTabController;
@override
Widget build(BuildContext context) {
Widget bottomBar;
if (_showBottomBar) {
List<Widget> bottomBarChildren = [];
if (_bottomBarText != null) {
bottomBarChildren.add(
Padding(
padding: EdgeInsets.fromLTRB(
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
Sizes.rowPadding),
child: Text(
"$_bottomBarText",
textAlign: TextAlign.left,
softWrap: true,
),
)
);
}
if (_bottomBarProgress) {
bottomBarChildren.add(
CollectionScaleTransition(
children: <Widget>[
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.on),),
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.unavailable),),
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.off),),
],
),
);
}
if (bottomBarChildren.isNotEmpty) {
bottomBar = Container(
color: _bottomBarColor,
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: bottomBarChildren,
),
),
_bottomBarAction
],
),
);
}
}
// This method is rerun every time setState is called.
if (widget.homeAssistant.isNoViews) {
return Scaffold(
key: _scaffoldKey,
primary: false,
drawer: _buildAppDrawer(),
bottomNavigationBar: bottomBar,
body: _buildScaffoldBody(true)
);
} else {
return Scaffold(
key: _scaffoldKey,
drawer: _buildAppDrawer(),
primary: false,
bottomNavigationBar: bottomBar,
body: HomeAssistantModel(
child: _buildScaffoldBody(false),
homeAssistant: widget.homeAssistant
),
);
}
}
@override
void dispose() {
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.dispose();
WidgetsBinding.instance.removeObserver(this);
_viewsTabController?.dispose();
_stateSubscription?.cancel();
_settingsSubscription?.cancel();
_serviceCallSubscription?.cancel();
_showEntityPageSubscription?.cancel();
_showErrorSubscription?.cancel();
_startAuthSubscription?.cancel();
_reloadUISubscription?.cancel();
//TODO disconnect
//widget.homeAssistant?.disconnect();
super.dispose();
}
}
}

View File

@ -1,4 +1,4 @@
part of 'main.dart';
part of '../main.dart';
class AuthManager {
@ -10,34 +10,35 @@ class AuthManager {
AuthManager._internal();
Future getTempToken({String httpWebHost, String oauthUrl}) {
Future getTempToken({String oauthUrl}) {
Completer completer = Completer();
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.onUrlChanged.listen((String url) {
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) {
String authCode = url.split("=")[1];
Logger.d("We have auth code. Getting temporary access token...");
Connection().sendHTTPPost(
host: httpWebHost,
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) {
Logger.d("Gottemp token");
Logger.d("Got temp token");
String tempToken = json.decode(response)['access_token'];
Logger.d("Closing webview...");
flutterWebviewPlugin.close();
//flutterWebviewPlugin.close();
eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.complete(tempToken);
}).catchError((e) {
flutterWebviewPlugin.close();
completer.completeError({"errorCode": 61, "errorMessage": "Error getting temp token"});
//flutterWebviewPlugin.close();
Logger.e("Error getting temp token: ${e.toString()}");
eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.completeError(HAError("Error getting temp token"));
});
}
});
Logger.d("Launching OAuth...");
eventBus.fire(StartAuthEvent(oauthUrl));
Logger.d("Launching OAuth");
eventBus.fire(StartAuthEvent(oauthUrl, true));
return completer.future;
}

View File

@ -0,0 +1,413 @@
part of '../main.dart';
class ConnectionManager {
static final ConnectionManager _instance = ConnectionManager._internal();
factory ConnectionManager() {
return _instance;
}
ConnectionManager._internal();
String _domain;
String _port;
String displayHostname;
String _webSocketAPIEndpoint;
String httpWebHost;
String _token;
String _tempToken;
String oauthUrl;
String webhookId;
bool useLovelace = true;
bool settingsLoaded = false;
bool get isAuthenticated => _token != null;
StreamSubscription _socketSubscription;
Duration connectTimeout = Duration(seconds: 15);
bool isConnected = false;
var onStateChangeCallback;
IOWebSocketChannel _socket;
int _currentMessageId = 0;
Map<String, Completer> _messageResolver = {};
Future init({bool loadSettings, bool forceReconnect: false}) async {
Completer completer = Completer();
bool stopInit = false;
if (loadSettings) {
Logger.e("Loading settings...");
SharedPreferences prefs = await SharedPreferences.getInstance();
useLovelace = prefs.getBool('use-lovelace') ?? true;
_domain = prefs.getString('hassio-domain');
_port = prefs.getString('hassio-port');
webhookId = prefs.getString('app-webhook-id');
displayHostname = "$_domain:$_port";
_webSocketAPIEndpoint =
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
httpWebHost =
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
if ((_domain == null) || (_port == null) ||
(_domain.isEmpty) || (_port.isEmpty)) {
completer.completeError(HAError.checkConnectionSettings());
stopInit = true;
} else {
final storage = new FlutterSecureStorage();
try {
_token = await storage.read(key: "hacl_llt");
Logger.e("Long-lived token read successful");
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
.encodeComponent(
'http://ha-client.homemade.systems/service/auth_callback.html')}";
settingsLoaded = true;
} catch (e) {
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
Logger.e("Cannt read secure storage. Need to relogin.");
stopInit = true;
}
}
} else {
if ((_domain == null) || (_port == null) ||
(_domain.isEmpty) || (_port.isEmpty)) {
completer.completeError(HAError.checkConnectionSettings());
stopInit = true;
}
}
if (!stopInit) {
if (_token == null) {
AuthManager().getTempToken(
oauthUrl: oauthUrl
).then((token) {
Logger.d("Token from AuthManager recived");
_tempToken = token;
_doConnect(completer: completer, forceReconnect: forceReconnect);
}).catchError((e) {
completer.completeError(e);
});
} else {
_doConnect(completer: completer, forceReconnect: forceReconnect);
}
}
return completer.future;
}
void _doConnect({Completer completer, bool forceReconnect}) {
if (forceReconnect || !isConnected) {
_connect().timeout(connectTimeout, onTimeout: () {
_disconnect().then((_) {
completer?.completeError(HAError("Connection timeout"));
});
}).then((_) {
completer?.complete();
}).catchError((e) {
completer?.completeError(e);
});
} else {
completer?.complete();
}
}
Completer connecting;
Future _connect() {
if (connecting != null && !connecting.isCompleted) {
Logger.w("Previous connection attempt pending...");
return connecting.future;
} else {
connecting = Completer();
_disconnect().then((_) {
Logger.d("Socket connecting...");
_socket = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
_socketSubscription = _socket.stream.listen(
(message) {
isConnected = true;
var data = json.decode(message);
if (data["type"] == "auth_required") {
Logger.d("[Received] <== ${data.toString()}");
_authenticate().then((_) {
Logger.d('Authentication complete');
connecting.complete();
}).catchError((e) {
if (!connecting.isCompleted) connecting.completeError(e);
});
} else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}");
_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);
}
},
cancelOnError: true,
onDone: () => _handleSocketClose(connecting),
onError: (e) => _handleSocketError(e, connecting)
);
});
return connecting.future;
}
}
Future _disconnect() {
Completer completer = Completer();
if (!isConnected) {
completer.complete();
} else {
isConnected = false;
List<Future> fl = [];
Logger.d("Socket disconnecting...");
if (_socketSubscription != null) {
fl.add(_socketSubscription.cancel());
}
if (_socket != null && _socket.sink != null &&
_socket.closeCode == null) {
fl.add(_socket.sink.close().timeout(Duration(seconds: 3)));
}
Future.wait(fl).whenComplete(() => completer.complete());
}
return completer.future;
}
_handleMessage(data) {
if (data["type"] == "result") {
if (data["id"] != null && data["success"]) {
Logger.d("[Received] <== Request id ${data['id']} was successful");
_messageResolver["${data["id"]}"]?.complete(data["result"]);
} else if (data["id"] != null) {
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
_messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}");
}
_messageResolver.remove("${data["id"]}");
} else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
onStateChangeCallback(data["event"]["data"]);
} else if (data["event"] != null) {
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
} else {
Logger.e("Event is null: $data");
}
} else {
Logger.d("[Received unhandled] <== ${data.toString()}");
}
}
void _handleSocketClose(Completer connectionCompleter) {
Logger.d("Socket disconnected.");
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect().catchError((e) {
isConnected = false;
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
});
});
});
}
}
void _handleSocketError(e, Completer connectionCompleter) {
Logger.e("Socket stream Error: $e");
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect().catchError((e) {
isConnected = false;
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
});
});
});
}
}
Future _authenticate() {
Completer completer = Completer();
if (_token != null) {
Logger.d( "Long-lived token exist");
Logger.d( "[Sending] ==> auth request");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_token"},
auth: true
).then((_) {
completer.complete();
}).catchError((e) => completer.completeError(e));
} else if (_tempToken != null) {
Logger.d("We have temp token. Loging in...");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_tempToken"},
auth: true
).then((_) {
Logger.d("Requesting long-lived token...");
_getLongLivedToken().then((_) {
Logger.d("getLongLivedToken finished");
completer.complete();
}).catchError((e) {
Logger.e("Can't get long-lived token: $e");
throw e;
});
}).catchError((e) => completer.completeError(e));
} else {
completer.completeError(HAError("General login error"));
}
return completer.future;
}
Future logout() {
Logger.d("Logging out");
Completer completer = Completer();
_disconnect().whenComplete(() {
_token = null;
_tempToken = null;
final storage = new FlutterSecureStorage();
storage.delete(key: "hacl_llt").whenComplete((){
completer.complete();
});
});
return completer.future;
}
Future _getLongLivedToken() {
Completer completer = Completer();
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
Logger.d("Got long-lived token.");
_token = data;
_tempToken = null;
final storage = new FlutterSecureStorage();
storage.write(key: "hacl_llt", value: "$_token").then((_) {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool("oauth-used", true);
completer.complete();
});
}).catchError((e) {
throw e;
});
}).catchError((e) {
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
});
return completer.future;
}
Future sendSocketMessage({String type, Map additionalData, bool auth: false}) {
Completer _completer = Completer();
Map dataObject = {"type": "$type"};
String callbackName;
if (!auth) {
_incrementMessageId();
dataObject["id"] = _currentMessageId;
callbackName = "$_currentMessageId";
} else {
callbackName = "auth";
}
if (additionalData != null) {
dataObject.addAll(additionalData);
}
_messageResolver[callbackName] = _completer;
String rawMessage = json.encode(dataObject);
if (!isConnected) {
_connect().timeout(connectTimeout, onTimeout: (){
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
}).then((_) {
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
_socket.sink.add(rawMessage);
}).catchError((e) {
_completer.completeError(e);
});
} else {
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
_socket.sink.add(rawMessage);
}
return _completer.future;
}
void _incrementMessageId() {
_currentMessageId += 1;
}
Future callService({String domain, String service, String entityId, Map additionalServiceData}) {
Map serviceData = {};
if (entityId != null) {
serviceData["entity_id"] = entityId;
}
if (additionalServiceData != null && additionalServiceData.isNotEmpty) {
serviceData.addAll(additionalServiceData);
}
if (serviceData.isNotEmpty)
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
else
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
}
Future<List> getHistory(String entityId) async {
DateTime now = DateTime.now();
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String url = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
Logger.d("[Sending] ==> HTTP /api/history/period/$startTime?&filter_entity_id=$entityId");
http.Response historyResponse;
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_token",
"Content-Type": "application/json"
});
var history = json.decode(historyResponse.body);
if (history is List) {
Logger.d( "[Received] <== HTTP ${history.first.length} history recors");
return history;
} else {
return [];
}
}
Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async {
Completer completer = Completer();
String url = "$httpWebHost$endPoint";
Logger.d("[Sending] ==> HTTP $endPoint");
Map<String, String> headers = {};
if (contentType != null) {
headers["Content-Type"] = contentType;
}
if (includeAuthHeader) {
headers["authorization"] = "Bearer $_token";
}
http.post(
url,
headers: headers,
body: data
).then((response) {
Logger.d("[Received] <== HTTP ${response.statusCode}");
if (response.statusCode >= 200 && response.statusCode < 300 ) {
completer.complete(response.body);
} else {
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
}
}).catchError((e) {
completer.completeError(e);
});
return completer.future;
}
}

View File

@ -0,0 +1,29 @@
part of '../main.dart';
class DeviceInfoManager {
static final DeviceInfoManager _instance = DeviceInfoManager._internal();
factory DeviceInfoManager() {
return _instance;
}
String unicDeviceId;
String manufacturer;
String model;
String osName;
String osVersion;
DeviceInfoManager._internal();
loadDeviceInfo() {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
deviceInfo.androidInfo.then((androidInfo) {
unicDeviceId = "${androidInfo.model.toLowerCase().replaceAll(' ', '_')}_${androidInfo.androidId}";
manufacturer = "${androidInfo.manufacturer}";
model = "${androidInfo.model}";
osName = "Android";
osVersion = "${androidInfo.version.release}";
});
}
}

View File

@ -0,0 +1,5 @@
part of '../main.dart';
class LocationManager {
}

View File

@ -0,0 +1,121 @@
part of '../main.dart';
class MobileAppIntegrationManager {
static final _appRegistrationData = {
"app_version": "$appVersion",
"device_name": "${HomeAssistant().userName}'s ${DeviceInfoManager().model}",
"manufacturer": DeviceInfoManager().manufacturer,
"model": DeviceInfoManager().model,
"os_version": DeviceInfoManager().osVersion,
"app_data": {
"push_token": "${HomeAssistant().fcmToken}",
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
}
};
static Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
Completer completer = Completer();
if (ConnectionManager().webhookId == null || forceRegister) {
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
var registrationData = Map.from(_appRegistrationData);
registrationData.addAll({
"app_id": "ha_client",
"app_name": "$appName",
"os_name": DeviceInfoManager().osName,
"supports_encryption": false,
});
ConnectionManager().sendHTTPPost(
endPoint: "/api/mobile_app/registrations",
includeAuthHeader: true,
data: json.encode(registrationData)
).then((response) {
Logger.d("Processing registration responce...");
var responseObject = json.decode(response);
SharedPreferences.getInstance().then((prefs) {
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
ConnectionManager().webhookId = responseObject["webhook_id"];
completer.complete();
eventBus.fire(ShowPopupDialogEvent(
title: "Mobile app Integration was created",
body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant",
positiveText: "Restart now",
negativeText: "Later",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "restart", entityId: null);
},
));
});
}).catchError((e) {
completer.complete();
Logger.e("Error registering the app: ${e.toString()}");
});
return completer.future;
} else {
Logger.d("App was previously registered. Checking...");
var updateData = {
"type": "update_registration",
"data": _appRegistrationData
};
ConnectionManager().sendHTTPPost(
endPoint: "/api/webhook/${ConnectionManager().webhookId}",
includeAuthHeader: false,
data: json.encode(updateData)
).then((response) {
if (response == null || response.isEmpty) {
Logger.d("No registration data in response. MobileApp integration was removed");
_askToRegisterApp();
} else {
Logger.d("App registration works fine");
if (showOkDialog) {
eventBus.fire(ShowPopupDialogEvent(
title: "All good",
body: "HA Client integration with your Home Assistant server works fine",
positiveText: "Nice!",
negativeText: "Ok"
));
}
}
completer.complete();
}).catchError((e) {
if (e['code'] != null && e['code'] == 410) {
Logger.e("MobileApp integration was removed");
_askToRegisterApp();
} else {
Logger.e("Error updating app registration: ${e.toString()}");
eventBus.fire(ShowPopupDialogEvent(
title: "App integration is not working properly",
body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue.",
positiveText: "Report to GitHub",
negativeText: "Report to Discord",
onPositive: () {
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
},
onNegative: () {
Launcher.launchURL("https://discord.gg/AUzEvwn");
},
));
}
completer.complete();
});
return completer.future;
}
}
static void _askToRegisterApp() {
eventBus.fire(ShowPopupDialogEvent(
title: "App integration was removed",
body: "Looks like app integration was removed from your Home Assistant. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and other useful stuff.",
positiveText: "Register now",
negativeText: "Cancel",
onPositive: () {
SharedPreferences.getInstance().then((prefs) {
prefs.remove("app-webhook-id");
ConnectionManager().webhookId = null;
checkAppRegistration();
});
},
));
}
}

View File

@ -0,0 +1,71 @@
part of '../main.dart';
class StartupUserMessagesManager {
static final StartupUserMessagesManager _instance = StartupUserMessagesManager
._internal();
factory StartupUserMessagesManager() {
return _instance;
}
StartupUserMessagesManager._internal() {}
bool _supportAppDevelopmentMessageShown;
bool _whatsNewMessageShown;
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
static final _whatsNewMessageKey = "user-message-shown-whats-new-660";
void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload();
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
_whatsNewMessageShown = prefs.getBool(_whatsNewMessageKey) ?? false;
if (!_whatsNewMessageShown) {
_showWhatsNewMessage();
} else if (!_supportAppDevelopmentMessageShown) {
_showSupportAppDevelopmentMessage();
}
}
void _showSupportAppDevelopmentMessage() {
eventBus.fire(ShowPopupDialogEvent(
title: "Hi!",
body: "As you may have noticed this app contains no ads. Also all app features are available for you for free. I'm not planning to change this in nearest future, but still you can support this application development materially. There is one-time payment available as well as several subscription options. Thanks.",
positiveText: "Show options",
negativeText: "Cancel",
onPositive: () {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_supportAppDevelopmentMessageKey, true);
eventBus.fire(ShowPageEvent(path: "/putchase"));
});
},
onNegative: () {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_supportAppDevelopmentMessageKey, true);
});
}
));
}
void _showWhatsNewMessage() {
eventBus.fire(ShowPopupDialogEvent(
title: "What's new",
body: "You can now share any media url to HA Client via Android share menu. It will try to play that media on one of your media player. There is also 'tv' button available in app header if you want to send some url manually",
positiveText: "Full release notes",
negativeText: "Ok",
onPositive: () {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_whatsNewMessageKey, true);
Launcher.launchURL("https://github.com/estevez-dev/ha_client/releases");
});
},
onNegative: () {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_whatsNewMessageKey, true);
});
}
));
}
}

View File

@ -24,10 +24,12 @@ class MaterialDesignIcons {
"cover.opening": "mdi:window-open",
"camera": "mdi:cctv",
"calendar": "mdi:calendar",
"device_tracker": "mdi:account",
"timer": "mdi:timer",
"lock.locked": "mdi:lock",
"lock.unlocked": "mdi:lock-open",
"fan": "mdi:fan",
"remote": "mdi:remote",
"alarm_control_panel.disarmed" : "mdi:bell-outline",
"alarm_control_panel.armed_home" : "mdi:bell-plus",
"alarm_control_panel.armed_away" : "mdi:bell",
@ -64,18 +66,18 @@ class MaterialDesignIcons {
"binary_sensor.occupancy.off": "mdi:home-outline",
"binary_sensor.opening.on": "mdi:square-outline",
"binary_sensor.opening.off": "mdi:square",
//"binary_sensor.plug.on": "mdi:",
//"binary_sensor.plug.off": "mdi:",
"binary_sensor.plug.on": "mdi:power-plug",
"binary_sensor.plug.off": "mdi:power-plug-off",
"binary_sensor.power.on": "mdi:alert",
"binary_sensor.power.off": "mdi:verified",
"binary_sensor.power.off": "mdi:shield-check",
//"binary_sensor.presence.on": "mdi:",
//"binary_sensor.presence.off": "mdi:",
"binary_sensor.problem.on": "mdi:alert-outline",
"binary_sensor.problem.off": "mdi:check-outline",
"binary_sensor.safety.on": "mdi:alert",
"binary_sensor.safety.off": "mdi:verified",
"binary_sensor.safety.off": "mdi:shield-check",
"binary_sensor.smoke.on": "mdi:alert",
"binary_sensor.smoke.off": "mdi:verified",
"binary_sensor.smoke.off": "mdi:shield-check",
"binary_sensor.sound.on": "mdi:music-note",
"binary_sensor.sound.off": "mdi:music-note-off",
"binary_sensor.vibration.on": "mdi:vibrate",
@ -96,8 +98,8 @@ class MaterialDesignIcons {
"cover.window.closing": "mdi:window-open",
"cover.window.opening": "mdi:window-open",
};
static final Map iconsDataMap = {
static final Map iconsDataMap = {
"mdi:access-point": 0xf002,
"mdi:access-point-network": 0xf003,
"mdi:access-point-network-off": 0xfbbd,
@ -111,6 +113,8 @@ class MaterialDesignIcons {
"mdi:account-badge": 0xfd83,
"mdi:account-badge-alert": 0xfd84,
"mdi:account-badge-alert-outline": 0xfd85,
"mdi:account-badge-horizontal": 0xfdf0,
"mdi:account-badge-horizontal-outline": 0xfdf1,
"mdi:account-badge-outline": 0xfd86,
"mdi:account-box": 0xf006,
"mdi:account-box-multiple": 0xf933,
@ -492,6 +496,7 @@ class MaterialDesignIcons {
"mdi:auto-upload": 0xf069,
"mdi:autorenew": 0xf06a,
"mdi:av-timer": 0xf06b,
"mdi:aws": 0xfdf2,
"mdi:axe": 0xf8c7,
"mdi:axis": 0xfd24,
"mdi:axis-arrow": 0xfd25,
@ -519,6 +524,10 @@ class MaterialDesignIcons {
"mdi:backspace-outline": 0xfb38,
"mdi:backup-restore": 0xf06f,
"mdi:badminton": 0xf850,
"mdi:bag-personal": 0xfdf3,
"mdi:bag-personal-off": 0xfdf4,
"mdi:bag-personal-off-outline": 0xfdf5,
"mdi:bag-personal-outline": 0xfdf6,
"mdi:balloon": 0xfa25,
"mdi:ballot": 0xf9c8,
"mdi:ballot-outline": 0xf9c9,
@ -626,6 +635,7 @@ class MaterialDesignIcons {
"mdi:bell-sleep-outline": 0xfa92,
"mdi:beta": 0xf0a1,
"mdi:betamax": 0xf9ca,
"mdi:biathlon": 0xfdf7,
"mdi:bible": 0xf0a2,
"mdi:bike": 0xf0a3,
"mdi:billiards": 0xfb3d,
@ -680,6 +690,8 @@ class MaterialDesignIcons {
"mdi:bookmark-check": 0xf0c1,
"mdi:bookmark-minus": 0xf9cb,
"mdi:bookmark-minus-outline": 0xf9cc,
"mdi:bookmark-multiple": 0xfdf8,
"mdi:bookmark-multiple-outline": 0xfdf9,
"mdi:bookmark-music": 0xf0c2,
"mdi:bookmark-off": 0xf9cd,
"mdi:bookmark-off-outline": 0xf9ce,
@ -791,6 +803,8 @@ class MaterialDesignIcons {
"mdi:calendar-heart": 0xf9d1,
"mdi:calendar-import": 0xfb0a,
"mdi:calendar-minus": 0xfd38,
"mdi:calendar-month": 0xfdfa,
"mdi:calendar-month-outline": 0xfdfb,
"mdi:calendar-multiple": 0xf0f1,
"mdi:calendar-multiple-check": 0xf0f2,
"mdi:calendar-multiselect": 0xfa31,
@ -837,6 +851,8 @@ class MaterialDesignIcons {
"mdi:camera-party-mode": 0xf105,
"mdi:camera-rear": 0xf106,
"mdi:camera-rear-variant": 0xf107,
"mdi:camera-retake": 0xfdfc,
"mdi:camera-retake-outline": 0xfdfd,
"mdi:camera-switch": 0xf108,
"mdi:camera-timer": 0xf109,
"mdi:camera-wireless": 0xfd92,
@ -847,6 +863,7 @@ class MaterialDesignIcons {
"mdi:cannabis": 0xf7a5,
"mdi:caps-lock": 0xfa9a,
"mdi:car": 0xf10b,
"mdi:car-back": 0xfdfe,
"mdi:car-battery": 0xf10c,
"mdi:car-brake-abs": 0xfc23,
"mdi:car-brake-alert": 0xfc24,
@ -868,6 +885,7 @@ class MaterialDesignIcons {
"mdi:car-light-high": 0xfc28,
"mdi:car-limousine": 0xf8cc,
"mdi:car-multiple": 0xfb4a,
"mdi:car-off": 0xfdff,
"mdi:car-parking-lights": 0xfd3f,
"mdi:car-pickup": 0xf7a9,
"mdi:car-side": 0xf7aa,
@ -916,6 +934,7 @@ class MaterialDesignIcons {
"mdi:cassette": 0xf9d3,
"mdi:cast": 0xf118,
"mdi:cast-connected": 0xf119,
"mdi:cast-education": 0xfe6d,
"mdi:cast-off": 0xf789,
"mdi:castle": 0xf11a,
"mdi:cat": 0xf11b,
@ -966,6 +985,7 @@ class MaterialDesignIcons {
"mdi:chat-processing": 0xfb57,
"mdi:check": 0xf12c,
"mdi:check-all": 0xf12d,
"mdi:check-bold": 0xfe6e,
"mdi:check-box-multiple-outline": 0xfc2d,
"mdi:check-box-outline": 0xfc2e,
"mdi:check-circle": 0xf5e0,
@ -974,6 +994,9 @@ class MaterialDesignIcons {
"mdi:check-network": 0xfc2f,
"mdi:check-network-outline": 0xfc30,
"mdi:check-outline": 0xf854,
"mdi:check-underline": 0xfe70,
"mdi:check-underline-circle": 0xfe71,
"mdi:check-underline-circle-outline": 0xfe72,
"mdi:checkbook": 0xfa9c,
"mdi:checkbox-blank": 0xf12e,
"mdi:checkbox-blank-circle": 0xf12f,
@ -1049,6 +1072,7 @@ class MaterialDesignIcons {
"mdi:circle-slice-7": 0xfaa3,
"mdi:circle-slice-8": 0xfaa4,
"mdi:circle-small": 0xf9de,
"mdi:circular-saw": 0xfe73,
"mdi:cisco-webex": 0xf145,
"mdi:city": 0xf146,
"mdi:city-variant": 0xfa35,
@ -1148,6 +1172,11 @@ class MaterialDesignIcons {
"mdi:collapse-all": 0xfaa5,
"mdi:collapse-all-outline": 0xfaa6,
"mdi:color-helper": 0xf179,
"mdi:comma": 0xfe74,
"mdi:comma-box": 0xfe75,
"mdi:comma-box-outline": 0xfe76,
"mdi:comma-circle": 0xfe77,
"mdi:comma-circle-outline": 0xfe78,
"mdi:comment": 0xf17a,
"mdi:comment-account": 0xf17b,
"mdi:comment-account-outline": 0xf17c,
@ -1201,6 +1230,8 @@ class MaterialDesignIcons {
"mdi:content-save-all": 0xf194,
"mdi:content-save-edit": 0xfcd7,
"mdi:content-save-edit-outline": 0xfcd8,
"mdi:content-save-move": 0xfe79,
"mdi:content-save-move-outline": 0xfe7a,
"mdi:content-save-outline": 0xf817,
"mdi:content-save-settings": 0xf61b,
"mdi:content-save-settings-outline": 0xfb13,
@ -1546,6 +1577,7 @@ class MaterialDesignIcons {
"mdi:file-cancel-outline": 0xfda3,
"mdi:file-chart": 0xf215,
"mdi:file-check": 0xf216,
"mdi:file-check-outline": 0xfe7b,
"mdi:file-cloud": 0xf217,
"mdi:file-compare": 0xf8a9,
"mdi:file-delimited": 0xf218,
@ -1573,9 +1605,11 @@ class MaterialDesignIcons {
"mdi:file-move": 0xfab8,
"mdi:file-multiple": 0xf222,
"mdi:file-music": 0xf223,
"mdi:file-music-outline": 0xfe7c,
"mdi:file-outline": 0xf224,
"mdi:file-pdf": 0xf225,
"mdi:file-pdf-box": 0xf226,
"mdi:file-pdf-outline": 0xfe7d,
"mdi:file-percent": 0xf81d,
"mdi:file-plus": 0xf751,
"mdi:file-powerpoint": 0xf227,
@ -1596,6 +1630,7 @@ class MaterialDesignIcons {
"mdi:file-upload": 0xfa4c,
"mdi:file-upload-outline": 0xfa4d,
"mdi:file-video": 0xf22b,
"mdi:file-video-outline": 0xfe10,
"mdi:file-word": 0xf22c,
"mdi:file-word-box": 0xf22d,
"mdi:file-xml": 0xf22e,
@ -1614,6 +1649,9 @@ class MaterialDesignIcons {
"mdi:fire-truck": 0xf8aa,
"mdi:firebase": 0xf966,
"mdi:firefox": 0xf239,
"mdi:fireplace": 0xfe11,
"mdi:fireplace-off": 0xfe12,
"mdi:firework": 0xfe13,
"mdi:fish": 0xf23a,
"mdi:flag": 0xf23b,
"mdi:flag-checkered": 0xf23c,
@ -1715,6 +1753,7 @@ class MaterialDesignIcons {
"mdi:format-bold": 0xf264,
"mdi:format-clear": 0xf265,
"mdi:format-color-fill": 0xf266,
"mdi:format-color-highlight": 0xfe14,
"mdi:format-color-text": 0xf69d,
"mdi:format-columns": 0xf8de,
"mdi:format-float-center": 0xf267,
@ -1769,6 +1808,7 @@ class MaterialDesignIcons {
"mdi:format-text": 0xf284,
"mdi:format-text-rotation-down": 0xfd4f,
"mdi:format-text-rotation-none": 0xfd50,
"mdi:format-text-variant": 0xfe15,
"mdi:format-text-wrapping-clip": 0xfcea,
"mdi:format-text-wrapping-overflow": 0xfceb,
"mdi:format-text-wrapping-wrap": 0xfcec,
@ -1805,6 +1845,22 @@ class MaterialDesignIcons {
"mdi:fuse": 0xfc61,
"mdi:fuse-blade": 0xfc62,
"mdi:gamepad": 0xf296,
"mdi:gamepad-circle": 0xfe16,
"mdi:gamepad-circle-down": 0xfe17,
"mdi:gamepad-circle-left": 0xfe18,
"mdi:gamepad-circle-outline": 0xfe19,
"mdi:gamepad-circle-right": 0xfe1a,
"mdi:gamepad-circle-up": 0xfe1b,
"mdi:gamepad-down": 0xfe1c,
"mdi:gamepad-left": 0xfe1d,
"mdi:gamepad-right": 0xfe1e,
"mdi:gamepad-round": 0xfe1f,
"mdi:gamepad-round-down": 0xfe7e,
"mdi:gamepad-round-left": 0xfe7f,
"mdi:gamepad-round-outline": 0xfe80,
"mdi:gamepad-round-right": 0xfe81,
"mdi:gamepad-round-up": 0xfe82,
"mdi:gamepad-up": 0xfe83,
"mdi:gamepad-variant": 0xf297,
"mdi:gantry-crane": 0xfdad,
"mdi:garage": 0xf6d8,
@ -1820,6 +1876,7 @@ class MaterialDesignIcons {
"mdi:gate-or": 0xf8e4,
"mdi:gate-xnor": 0xf8e5,
"mdi:gate-xor": 0xf8e6,
"mdi:gatsby": 0xfe84,
"mdi:gauge": 0xf29a,
"mdi:gauge-empty": 0xf872,
"mdi:gauge-full": 0xf873,
@ -1848,7 +1905,8 @@ class MaterialDesignIcons {
"mdi:ghost": 0xf2a0,
"mdi:ghost-off": 0xf9f4,
"mdi:gif": 0xfd54,
"mdi:gift": 0xf2a1,
"mdi:gift": 0xfe85,
"mdi:gift-outline": 0xf2a1,
"mdi:git": 0xf2a2,
"mdi:github-box": 0xf2a3,
"mdi:github-circle": 0xf2a4,
@ -1915,6 +1973,7 @@ class MaterialDesignIcons {
"mdi:grid": 0xf2c1,
"mdi:grid-large": 0xf757,
"mdi:grid-off": 0xf2c2,
"mdi:grill": 0xfe86,
"mdi:group": 0xf2c3,
"mdi:guitar-acoustic": 0xf770,
"mdi:guitar-electric": 0xf2c4,
@ -1927,6 +1986,7 @@ class MaterialDesignIcons {
"mdi:hamburger": 0xf684,
"mdi:hammer": 0xf8e9,
"mdi:hand": 0xfa4e,
"mdi:hand-left": 0xfe87,
"mdi:hand-okay": 0xfa4f,
"mdi:hand-peace": 0xfa50,
"mdi:hand-peace-variant": 0xfa51,
@ -1934,6 +1994,8 @@ class MaterialDesignIcons {
"mdi:hand-pointing-left": 0xfa53,
"mdi:hand-pointing-right": 0xf2c7,
"mdi:hand-pointing-up": 0xfa54,
"mdi:hand-right": 0xfe88,
"mdi:hand-saw": 0xfe89,
"mdi:hanger": 0xf2c8,
"mdi:hard-hat": 0xf96e,
"mdi:harddisk": 0xf2ca,
@ -2070,6 +2132,7 @@ class MaterialDesignIcons {
"mdi:image-filter-none": 0xf2f6,
"mdi:image-filter-tilt-shift": 0xf2f7,
"mdi:image-filter-vintage": 0xf2f8,
"mdi:image-frame": 0xfe8a,
"mdi:image-move": 0xf9f7,
"mdi:image-multiple": 0xf2f9,
"mdi:image-off": 0xf82a,
@ -2095,6 +2158,7 @@ class MaterialDesignIcons {
"mdi:instapaper": 0xf2ff,
"mdi:internet-explorer": 0xf300,
"mdi:invert-colors": 0xf301,
"mdi:invert-colors-off": 0xfe8b,
"mdi:ip": 0xfa5e,
"mdi:ip-network": 0xfa5f,
"mdi:ip-network-outline": 0xfc6c,
@ -2124,6 +2188,7 @@ class MaterialDesignIcons {
"mdi:keyboard-caps": 0xf30e,
"mdi:keyboard-close": 0xf30f,
"mdi:keyboard-off": 0xf310,
"mdi:keyboard-off-outline": 0xfe8c,
"mdi:keyboard-outline": 0xf97a,
"mdi:keyboard-return": 0xf311,
"mdi:keyboard-settings": 0xf9f8,
@ -2175,9 +2240,12 @@ class MaterialDesignIcons {
"mdi:launch": 0xf327,
"mdi:lava-lamp": 0xf7d4,
"mdi:layers": 0xf328,
"mdi:layers-minus": 0xfe8d,
"mdi:layers-off": 0xf329,
"mdi:layers-off-outline": 0xf9fc,
"mdi:layers-outline": 0xf9fd,
"mdi:layers-plus": 0xfe30,
"mdi:layers-remove": 0xfe31,
"mdi:lead-pencil": 0xf64f,
"mdi:leaf": 0xf32a,
"mdi:leaf-maple": 0xfc6f,
@ -2202,6 +2270,8 @@ class MaterialDesignIcons {
"mdi:lifebuoy": 0xf87d,
"mdi:light-switch": 0xf97d,
"mdi:lightbulb": 0xf335,
"mdi:lightbulb-off": 0xfe32,
"mdi:lightbulb-off-outline": 0xfe33,
"mdi:lightbulb-on": 0xf6e7,
"mdi:lightbulb-on-outline": 0xf6e8,
"mdi:lightbulb-outline": 0xf336,
@ -2374,6 +2444,7 @@ class MaterialDesignIcons {
"mdi:monitor-lock": 0xfdb7,
"mdi:monitor-multiple": 0xf37a,
"mdi:monitor-off": 0xfd6c,
"mdi:monitor-screenshot": 0xfe34,
"mdi:monitor-star": 0xfdb8,
"mdi:more": 0xf37b,
"mdi:mother-nurse": 0xfcfd,
@ -2437,8 +2508,11 @@ class MaterialDesignIcons {
"mdi:new-box": 0xf394,
"mdi:newspaper": 0xf395,
"mdi:nfc": 0xf396,
"mdi:nfc-off": 0xfe35,
"mdi:nfc-search-variant": 0xfe36,
"mdi:nfc-tap": 0xf397,
"mdi:nfc-variant": 0xf398,
"mdi:nfc-variant-off": 0xfe37,
"mdi:ninja": 0xf773,
"mdi:nintendo-switch": 0xf7e0,
"mdi:nodejs": 0xf399,
@ -2452,6 +2526,7 @@ class MaterialDesignIcons {
"mdi:note-plus-outline": 0xf39d,
"mdi:note-text": 0xf39e,
"mdi:notebook": 0xf82d,
"mdi:notebook-multiple": 0xfe38,
"mdi:notification-clear-all": 0xf39f,
"mdi:npm": 0xf6f6,
"mdi:npm-variant": 0xf98e,
@ -2573,7 +2648,7 @@ class MaterialDesignIcons {
"mdi:page-previous-outline": 0xfb8f,
"mdi:palette": 0xf3d8,
"mdi:palette-advanced": 0xf3d9,
"mdi:palette-outline": 0xfde8,
"mdi:palette-outline": 0xfe6c,
"mdi:palette-swatch": 0xf8b4,
"mdi:pan": 0xfb90,
"mdi:pan-bottom-left": 0xfb91,
@ -2609,6 +2684,7 @@ class MaterialDesignIcons {
"mdi:paw": 0xf3e9,
"mdi:paw-off": 0xf657,
"mdi:paypal": 0xf882,
"mdi:pdf-box": 0xfe39,
"mdi:peace": 0xf883,
"mdi:pen": 0xf3ea,
"mdi:pen-lock": 0xfdbe,
@ -2667,6 +2743,10 @@ class MaterialDesignIcons {
"mdi:pi-hole": 0xfdcd,
"mdi:piano": 0xf67c,
"mdi:pickaxe": 0xf8b6,
"mdi:picture-in-picture-bottom-right": 0xfe3a,
"mdi:picture-in-picture-bottom-right-outline": 0xfe3b,
"mdi:picture-in-picture-top-right": 0xfe3c,
"mdi:picture-in-picture-top-right-outline": 0xfe3d,
"mdi:pier": 0xf886,
"mdi:pier-crane": 0xf887,
"mdi:pig": 0xf401,
@ -2762,7 +2842,10 @@ class MaterialDesignIcons {
"mdi:presentation-play": 0xf429,
"mdi:printer": 0xf42a,
"mdi:printer-3d": 0xf42b,
"mdi:printer-3d-nozzle": 0xfe3e,
"mdi:printer-3d-nozzle-outline": 0xfe3f,
"mdi:printer-alert": 0xf42c,
"mdi:printer-off": 0xfe40,
"mdi:printer-settings": 0xf706,
"mdi:printer-wireless": 0xfa0a,
"mdi:priority-high": 0xf603,
@ -2822,6 +2905,8 @@ class MaterialDesignIcons {
"mdi:record": 0xf44a,
"mdi:record-player": 0xf999,
"mdi:record-rec": 0xf44b,
"mdi:rectangle": 0xfe41,
"mdi:rectangle-outline": 0xfe42,
"mdi:recycle": 0xf44c,
"mdi:reddit": 0xf44d,
"mdi:redo": 0xf44e,
@ -2866,6 +2951,7 @@ class MaterialDesignIcons {
"mdi:ribbon": 0xf460,
"mdi:rice": 0xf7e9,
"mdi:ring": 0xf7ea,
"mdi:rivet": 0xfe43,
"mdi:road": 0xf461,
"mdi:road-variant": 0xf462,
"mdi:robot": 0xf6a8,
@ -2908,6 +2994,7 @@ class MaterialDesignIcons {
"mdi:satellite-uplink": 0xf908,
"mdi:satellite-variant": 0xf471,
"mdi:sausage": 0xf8b9,
"mdi:saw-blade": 0xfe44,
"mdi:saxophone": 0xf609,
"mdi:scale": 0xf472,
"mdi:scale-balance": 0xf5d1,
@ -2919,10 +3006,10 @@ class MaterialDesignIcons {
"mdi:screen-rotation": 0xf475,
"mdi:screen-rotation-lock": 0xf476,
"mdi:screw-flat-top": 0xfdcf,
"mdi:screw-lag": 0xfdd0,
"mdi:screw-machine-flat-top": 0xfdd1,
"mdi:screw-machine-round-top": 0xfdd2,
"mdi:screw-round-top": 0xfdd3,
"mdi:screw-lag": 0xfe54,
"mdi:screw-machine-flat-top": 0xfe55,
"mdi:screw-machine-round-top": 0xfe56,
"mdi:screw-round-top": 0xfe57,
"mdi:screwdriver": 0xf477,
"mdi:script": 0xfb9d,
"mdi:script-outline": 0xf478,
@ -2944,6 +3031,8 @@ class MaterialDesignIcons {
"mdi:seatbelt": 0xfca1,
"mdi:security": 0xf483,
"mdi:security-network": 0xf484,
"mdi:seed": 0xfe45,
"mdi:seed-outline": 0xfe46,
"mdi:select": 0xf485,
"mdi:select-all": 0xf486,
"mdi:select-color": 0xfd0d,
@ -2956,8 +3045,8 @@ class MaterialDesignIcons {
"mdi:selection-ellipse": 0xfd0e,
"mdi:selection-off": 0xf776,
"mdi:send": 0xf48a,
"mdi:send-circle": 0xfdd4,
"mdi:send-circle-outline": 0xfdd5,
"mdi:send-circle": 0xfe58,
"mdi:send-circle-outline": 0xfe59,
"mdi:send-lock": 0xf7ec,
"mdi:serial-port": 0xf65c,
"mdi:server": 0xf48b,
@ -3021,7 +3110,7 @@ class MaterialDesignIcons {
"mdi:ship-wheel": 0xf832,
"mdi:shoe-formal": 0xfb22,
"mdi:shoe-heel": 0xfb23,
"mdi:shoe-print": 0xfdd6,
"mdi:shoe-print": 0xfe5a,
"mdi:shopify": 0xfadd,
"mdi:shopping": 0xf49a,
"mdi:shopping-music": 0xf49b,
@ -3047,14 +3136,15 @@ class MaterialDesignIcons {
"mdi:signal-cellular-2": 0xf8bc,
"mdi:signal-cellular-3": 0xf8bd,
"mdi:signal-cellular-outline": 0xf8be,
"mdi:signal-distance-variant": 0xfe47,
"mdi:signal-hspa": 0xf714,
"mdi:signal-hspa-plus": 0xf715,
"mdi:signal-off": 0xf782,
"mdi:signal-variant": 0xf60a,
"mdi:signature": 0xfdd7,
"mdi:signature-freehand": 0xfdd8,
"mdi:signature-image": 0xfdd9,
"mdi:signature-text": 0xfdda,
"mdi:signature": 0xfe5b,
"mdi:signature-freehand": 0xfe5c,
"mdi:signature-image": 0xfe5d,
"mdi:signature-text": 0xfe5e,
"mdi:silo": 0xfb24,
"mdi:silverware": 0xf4a3,
"mdi:silverware-fork": 0xf4a4,
@ -3087,8 +3177,8 @@ class MaterialDesignIcons {
"mdi:slackware": 0xf90a,
"mdi:sleep": 0xf4b2,
"mdi:sleep-off": 0xf4b3,
"mdi:slope-downhill": 0xfddb,
"mdi:slope-uphill": 0xfddc,
"mdi:slope-downhill": 0xfe5f,
"mdi:slope-uphill": 0xfe60,
"mdi:smog": 0xfa70,
"mdi:smoke-detector": 0xf392,
"mdi:smoking": 0xf4b4,
@ -3129,6 +3219,7 @@ class MaterialDesignIcons {
"mdi:spa": 0xfcad,
"mdi:spa-outline": 0xfcae,
"mdi:space-invaders": 0xfba5,
"mdi:spade": 0xfe48,
"mdi:speaker": 0xf4c3,
"mdi:speaker-bluetooth": 0xf9a1,
"mdi:speaker-multiple": 0xfd14,
@ -3142,6 +3233,8 @@ class MaterialDesignIcons {
"mdi:spotlight-beam": 0xf4c9,
"mdi:spray": 0xf665,
"mdi:spray-bottle": 0xfadf,
"mdi:sprout": 0xfe49,
"mdi:sprout-outline": 0xfe4a,
"mdi:square": 0xf763,
"mdi:square-edit-outline": 0xf90b,
"mdi:square-inc": 0xf4ca,
@ -3246,6 +3339,7 @@ class MaterialDesignIcons {
"mdi:table-row-remove": 0xf4f5,
"mdi:table-search": 0xf90e,
"mdi:table-settings": 0xf837,
"mdi:table-tennis": 0xfe4b,
"mdi:tablet": 0xf4f6,
"mdi:tablet-android": 0xf4f7,
"mdi:tablet-cellphone": 0xf9a6,
@ -3301,12 +3395,12 @@ class MaterialDesignIcons {
"mdi:theater": 0xf50d,
"mdi:theme-light-dark": 0xf50e,
"mdi:thermometer": 0xf50f,
"mdi:thermometer-alert": 0xfddd,
"mdi:thermometer-chevron-down": 0xfdde,
"mdi:thermometer-chevron-up": 0xfddf,
"mdi:thermometer-alert": 0xfe61,
"mdi:thermometer-chevron-down": 0xfe62,
"mdi:thermometer-chevron-up": 0xfe63,
"mdi:thermometer-lines": 0xf510,
"mdi:thermometer-minus": 0xfde0,
"mdi:thermometer-plus": 0xfde1,
"mdi:thermometer-minus": 0xfe64,
"mdi:thermometer-plus": 0xfe65,
"mdi:thermostat": 0xf393,
"mdi:thermostat-box": 0xf890,
"mdi:thought-bubble": 0xf7f5,
@ -3384,12 +3478,13 @@ class MaterialDesignIcons {
"mdi:transition": 0xf914,
"mdi:transition-masked": 0xf915,
"mdi:translate": 0xf5ca,
"mdi:translate-off": 0xfde2,
"mdi:translate-off": 0xfe66,
"mdi:transmission-tower": 0xfd1a,
"mdi:trash-can": 0xfa78,
"mdi:trash-can-outline": 0xfa79,
"mdi:treasure-chest": 0xf725,
"mdi:tree": 0xf531,
"mdi:tree-outline": 0xfe4c,
"mdi:trello": 0xf532,
"mdi:trending-down": 0xf533,
"mdi:trending-neutral": 0xf534,
@ -3450,7 +3545,7 @@ class MaterialDesignIcons {
"mdi:upload-multiple": 0xf83c,
"mdi:upload-network": 0xf6f5,
"mdi:upload-network-outline": 0xfcb4,
"mdi:upload-outline": 0xfde3,
"mdi:upload-outline": 0xfe67,
"mdi:usb": 0xf553,
"mdi:van-passenger": 0xf7f9,
"mdi:van-utility": 0xf7fa,
@ -3503,6 +3598,9 @@ class MaterialDesignIcons {
"mdi:view-array": 0xf56b,
"mdi:view-carousel": 0xf56c,
"mdi:view-column": 0xf56d,
"mdi:view-comfy": 0xfe4d,
"mdi:view-compact": 0xfe4e,
"mdi:view-compact-outline": 0xfe4f,
"mdi:view-dashboard": 0xf56e,
"mdi:view-dashboard-outline": 0xfa1c,
"mdi:view-dashboard-variant": 0xf842,
@ -3537,11 +3635,12 @@ class MaterialDesignIcons {
"mdi:volume-mute": 0xf75e,
"mdi:volume-off": 0xf581,
"mdi:volume-plus": 0xf75c,
"mdi:volume-variant-off": 0xfde4,
"mdi:volume-variant-off": 0xfe68,
"mdi:vote": 0xfa1e,
"mdi:vote-outline": 0xfa1f,
"mdi:vpn": 0xf582,
"mdi:vuejs": 0xf843,
"mdi:vuetify": 0xfe50,
"mdi:walk": 0xf583,
"mdi:wall": 0xf7fd,
"mdi:wall-sconce": 0xf91b,
@ -3552,7 +3651,7 @@ class MaterialDesignIcons {
"mdi:wallet-membership": 0xf586,
"mdi:wallet-outline": 0xfbb9,
"mdi:wallet-travel": 0xf587,
"mdi:wallpaper": 0xfde5,
"mdi:wallpaper": 0xfe69,
"mdi:wan": 0xf588,
"mdi:washing-machine": 0xf729,
"mdi:watch": 0xf589,
@ -3565,13 +3664,14 @@ class MaterialDesignIcons {
"mdi:watch-vibrate-off": 0xfcb6,
"mdi:water": 0xf58c,
"mdi:water-off": 0xf58d,
"mdi:water-outline": 0xfde6,
"mdi:water-outline": 0xfe6a,
"mdi:water-percent": 0xf58e,
"mdi:water-pump": 0xf58f,
"mdi:watermark": 0xf612,
"mdi:waves": 0xf78c,
"mdi:waze": 0xfbba,
"mdi:weather-cloudy": 0xf590,
"mdi:weather-cloudy-arrow-right": 0xfe51,
"mdi:weather-fog": 0xf591,
"mdi:weather-hail": 0xf592,
"mdi:weather-hurricane": 0xf897,
@ -3608,7 +3708,7 @@ class MaterialDesignIcons {
"mdi:widgets": 0xf72b,
"mdi:wifi": 0xf5a9,
"mdi:wifi-off": 0xf5aa,
"mdi:wifi-star": 0xfde7,
"mdi:wifi-star": 0xfe6b,
"mdi:wifi-strength-1": 0xf91e,
"mdi:wifi-strength-1-alert": 0xf91f,
"mdi:wifi-strength-1-lock": 0xf920,
@ -3659,7 +3759,9 @@ class MaterialDesignIcons {
"mdi:xbox-controller-battery-low": 0xf74d,
"mdi:xbox-controller-battery-medium": 0xf74e,
"mdi:xbox-controller-battery-unknown": 0xf74f,
"mdi:xbox-controller-menu": 0xfe52,
"mdi:xbox-controller-off": 0xf5bb,
"mdi:xbox-controller-view": 0xfe53,
"mdi:xda": 0xf5bc,
"mdi:xing": 0xf5bd,
"mdi:xing-box": 0xf5be,

View File

@ -1,10 +1,9 @@
part of 'main.dart';
part of '../main.dart';
class EntityViewPage extends StatefulWidget {
EntityViewPage({Key key, @required this.entityId, @required this.homeAssistant }) : super(key: key);
EntityViewPage({Key key, @required this.entityId}) : super(key: key);
final String entityId;
final HomeAssistant homeAssistant;
@override
_EntityViewPageState createState() => new _EntityViewPageState();
@ -31,7 +30,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
}
void _prepareData() async {
_title = widget.homeAssistant.entities.get(widget.entityId).displayName;
_title = HomeAssistant().entities.get(widget.entityId).displayName;
}
@ -46,10 +45,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
// the App.build method, and use it to set our appbar title.
title: new Text(_title),
),
body: HomeAssistantModel(
homeAssistant: widget.homeAssistant,
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
),
body: HomeAssistant().entities.get(widget.entityId).buildEntityPageWidget(context),
);
}

View File

@ -1,4 +1,4 @@
part of 'main.dart';
part of '../main.dart';
class LogViewPage extends StatefulWidget {
LogViewPage({Key key, this.title}) : super(key: key);

820
lib/pages/main.page.dart Normal file
View File

@ -0,0 +1,820 @@
part of '../main.dart';
class MainPage extends StatefulWidget {
MainPage({Key key, this.title}) : super(key: key);
final String title;
@override
_MainPageState createState() => new _MainPageState();
}
class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
StreamSubscription<List<PurchaseDetails>> _subscription;
StreamSubscription _stateSubscription;
StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription;
StreamSubscription _showErrorSubscription;
StreamSubscription _startAuthSubscription;
StreamSubscription _showPopupDialogSubscription;
StreamSubscription _showPopupMessageSubscription;
StreamSubscription _reloadUISubscription;
StreamSubscription _showPageSubscription;
int _previousViewCount;
bool _showLoginButton = false;
bool _preventAppRefresh = false;
String _savedSharedText;
@override
void initState() {
final Stream purchaseUpdates =
InAppPurchaseConnection.instance.purchaseUpdatedStream;
_subscription = purchaseUpdates.listen((purchases) {
_handlePurchaseUpdates(purchases);
});
super.initState();
enableShareReceiving();
WidgetsBinding.instance.addObserver(this);
_firebaseMessaging.configure(
onLaunch: (data) {
Logger.d("Notification [onLaunch]: $data");
return Future.value();
},
onMessage: (data) {
Logger.d("Notification [onMessage]: $data");
return _showNotification(title: data["notification"]["title"], text: data["notification"]["body"]);
},
onResume: (data) {
Logger.d("Notification [onResume]: $data");
return Future.value();
}
);
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
var initializationSettingsAndroid =
new AndroidInitializationSettings('mini_icon');
var initializationSettingsIOS = new IOSInitializationSettings(
onDidReceiveLocalNotification: null);
var initializationSettings = new InitializationSettings(
initializationSettingsAndroid, initializationSettingsIOS);
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: onSelectNotification);
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
Logger.d("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) {
_preventAppRefresh = false;
_fullLoad();
}
});
_fullLoad();
}
@override void receiveShare(Share shared) {
if (shared.mimeType == ShareType.TYPE_PLAIN_TEXT) {
_savedSharedText = shared.text;
}
}
Future onSelectNotification(String payload) async {
if (payload != null) {
Logger.d('Notification clicked: ' + payload);
}
}
Future _showNotification({String title, String text}) async {
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service',
importance: Importance.Max, priority: Priority.High);
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
var platformChannelSpecifics = new NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
title ?? appName,
text,
platformChannelSpecifics
);
}
void _fullLoad() async {
_showInfoBottomBar(progress: true,);
_subscribe().then((_) {
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
_fetchData();
StartupUserMessagesManager().checkMessagesToShow();
}, onError: (e) {
_setErrorState(e);
});
});
}
void _quickLoad() {
_hideBottomBar();
_showInfoBottomBar(progress: true,);
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
_fetchData();
//StartupUserMessagesManager().checkMessagesToShow();
}, onError: (e) {
_setErrorState(e);
});
}
_fetchData() async {
if (_savedSharedText != null && !HomeAssistant().isNoEntities) {
Logger.d("Got shared text: $_savedSharedText");
Navigator.pushNamed(context, "/play-media", arguments: {"url": _savedSharedText});
_savedSharedText = null;
}
await HomeAssistant().fetchData().then((_) {
_hideBottomBar();
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
if (_previousViewCount != currentViewCount) {
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
_viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount;
}
}).catchError((e) {
if (e is HAError) {
_setErrorState(e);
} else {
_setErrorState(HAError(e.toString()));
}
});
eventBus.fire(RefreshDataFinishedEvent());
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
Logger.d("$state");
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
_quickLoad();
}
}
void _handlePurchaseUpdates(purchase) {
if (purchase is List<PurchaseDetails>) {
if (purchase[0].status == PurchaseStatus.purchased) {
eventBus.fire(ShowPopupMessageEvent(
title: "Thanks a lot!",
body: "Thank you for supporting HA Client development!",
buttonText: "Ok"
));
} else {
Logger.d("Purchase change handler: ${purchase[0].status}");
}
} else {
Logger.e("Something wrong with purchase handling. Got: $purchase");
}
}
Future _subscribe() {
Completer completer = Completer();
if (_stateSubscription == null) {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) {
Logger.d("New entity. Need to rebuild UI");
_quickLoad();
} else {
setState(() {});
}
});
}
if (_reloadUISubscription == null) {
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
_quickLoad();
});
}
if (_showPopupDialogSubscription == null) {
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().listen((event){
_showPopupDialog(
title: event.title,
body: event.body,
onPositive: event.onPositive,
onNegative: event.onNegative,
positiveText: event.positiveText,
negativeText: event.negativeText
);
});
}
if (_showPopupMessageSubscription == null) {
_showPopupMessageSubscription = eventBus.on<ShowPopupMessageEvent>().listen((event){
_showPopupDialog(
title: event.title,
body: event.body,
onPositive: event.onButtonClick,
positiveText: event.buttonText,
negativeText: null
);
});
}
if (_serviceCallSubscription == null) {
_serviceCallSubscription =
eventBus.on<ServiceCallEvent>().listen((event) {
_callService(event.domain, event.service, event.entityId,
event.additionalParams);
});
}
if (_showEntityPageSubscription == null) {
_showEntityPageSubscription =
eventBus.on<ShowEntityPageEvent>().listen((event) {
_showEntityPage(event.entity.entityId);
});
}
if (_showPageSubscription == null) {
_showPageSubscription =
eventBus.on<ShowPageEvent>().listen((event) {
_showPage(event.path, event.goBackFirst);
});
}
if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_showErrorBottomBar(event.error);
});
}
if (_startAuthSubscription == null) {
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
setState(() {
_showLoginButton = event.showButton;
});
if (event.showButton) {
_showOAuth();
} else {
_preventAppRefresh = false;
Navigator.of(context).pop();
}
});
}
_firebaseMessaging.getToken().then((String token) {
HomeAssistant().fcmToken = token;
completer.complete();
});
return completer.future;
}
void _showOAuth() {
_preventAppRefresh = true;
Navigator.of(context).pushNamed('/login');
}
_setErrorState(HAError e) {
if (e == null) {
_showErrorBottomBar(
HAError("Unknown error")
);
} else {
_showErrorBottomBar(e);
}
}
void _showPopupDialog({String title, String body, var onPositive, var onNegative, String positiveText, String negativeText}) {
List<Widget> buttons = [];
buttons.add(FlatButton(
child: new Text("$positiveText"),
onPressed: () {
Navigator.of(context).pop();
if (onPositive != null) {
onPositive();
}
},
));
if (negativeText != null) {
buttons.add(FlatButton(
child: new Text("$negativeText"),
onPressed: () {
Navigator.of(context).pop();
if (onNegative != null) {
onNegative();
}
},
));
}
// flutter defined function
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
title: new Text("$title"),
content: new Text("$body"),
actions: buttons,
);
},
);
}
//TODO remove this shit
void _callService(String domain, String service, String entityId, Map additionalParams) {
_showInfoBottomBar(
message: "Calling $domain.$service",
duration: Duration(seconds: 3)
);
ConnectionManager().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
}
void _showEntityPage(String entityId) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EntityViewPage(entityId: entityId),
)
);
}
void _showPage(String path, bool goBackFirst) {
if (goBackFirst) {
Navigator.pop(context);
}
Navigator.pushNamed(
context,
path
);
}
List<Tab> buildUIViewTabs() {
List<Tab> result = [];
if (HomeAssistant().ui.views.isNotEmpty) {
HomeAssistant().ui.views.forEach((HAView view) {
result.add(view.buildTab());
});
}
return result;
}
Drawer _buildAppDrawer() {
List<Widget> menuItems = [];
menuItems.add(
UserAccountsDrawerHeader(
accountName: Text(HomeAssistant().userName),
accountEmail: Text(ConnectionManager().displayHostname ?? "Not configured"),
onDetailsPressed: () {
final flutterWebViewPlugin = new FlutterWebviewPlugin();
flutterWebViewPlugin.onStateChanged.listen((viewState) async {
if (viewState.type == WebViewState.startLoad) {
Logger.d("[WebView] Injecting external auth JS");
rootBundle.loadString('assets/js/externalAuth.js').then((js){
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token));
});
}
});
Navigator.of(context).pushNamed(
"/webview",
arguments: {
"url": "${ConnectionManager().httpWebHost}/profile?external_auth=1",
"title": "Profile"
}
);
},
currentAccountPicture: CircleAvatar(
child: Text(
HomeAssistant().userAvatarText,
style: TextStyle(
fontSize: 32.0
),
),
),
)
);
if (HomeAssistant().panels.isNotEmpty) {
HomeAssistant().panels.forEach((Panel panel) {
if (!panel.isHidden) {
menuItems.add(
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
title: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("${panel.title}"),
Container(width: 4.0,),
panel.isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
],
),
onTap: () {
Navigator.of(context).pop();
panel.handleOpen(context);
}
)
);
}
});
}
menuItems.addAll([
Divider(),
ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
title: Text("Connection settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/connection-settings');
},
)
]);
menuItems.addAll([
Divider(),
new ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text("Log"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/log-view');
},
),
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
title: Text("Report an issue"),
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
},
),
Divider(),
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:food")),
title: Text("Support app development"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/putchase');
},
),
Divider(),
new ListTile(
leading: Icon(Icons.help),
title: Text("Help"),
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("http://ha-client.homemade.systems/docs");
},
),
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
title: Text("Join Discord channel"),
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("https://discord.gg/AUzEvwn");
},
),
new AboutListTile(
aboutBoxChildren: <Widget>[
GestureDetector(
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("http://ha-client.homemade.systems/");
},
child: Text(
"ha-client.homemade.systems",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
),
),
),
Container(
height: 10.0,
),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions");
},
child: Text(
"Terms and Conditions",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
),
),
),
Container(
height: 10.0,
),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy");
},
child: Text(
"Privacy Policy",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
),
),
)
],
applicationName: appName,
applicationVersion: appVersion
)
]);
return new Drawer(
child: ListView(
children: menuItems,
),
);
}
void _hideBottomBar() {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
setState(() {
_showBottomBar = false;
});
}
Widget _bottomBarAction;
bool _showBottomBar = false;
String _bottomBarText;
bool _bottomBarProgress;
Color _bottomBarColor;
Timer _bottomBarTimer;
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
_bottomBarTimer?.cancel();
_bottomBarAction = Container(height: 0.0, width: 0.0,);
_bottomBarColor = Colors.grey.shade50;
setState(() {
_bottomBarText = message;
_bottomBarProgress = progress;
_showBottomBar = true;
});
if (duration != null) {
_bottomBarTimer = Timer(duration, () {
_hideBottomBar();
});
}
}
void _showErrorBottomBar(HAError error) {
TextStyle textStyle = TextStyle(
color: Colors.blue,
fontSize: Sizes.nameFontSize
);
_bottomBarColor = Colors.red.shade100;
List<Widget> actions = [];
error.actions.forEach((HAErrorAction action) {
switch (action.type) {
case HAErrorActionType.FULL_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
_fullLoad();
},
));
break;
}
case HAErrorActionType.QUICK_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
_quickLoad();
},
));
break;
}
case HAErrorActionType.RELOGIN: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
ConnectionManager().logout().then((_) => _fullLoad());
},
));
break;
}
case HAErrorActionType.URL: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
Launcher.launchURLInCustomTab(context: context, url: "${action.url}");
},
));
break;
}
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
Navigator.pushNamed(context, '/connection-settings');
},
));
break;
}
}
});
if (actions.isNotEmpty) {
_bottomBarAction = Row(
mainAxisSize: MainAxisSize.min,
children: actions,
mainAxisAlignment: MainAxisAlignment.end,
);
} else {
_bottomBarAction = Container(height: 0.0, width: 0.0,);
}
setState(() {
_bottomBarProgress = false;
_bottomBarText = "${error.message}";
_showBottomBar = true;
});
}
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> popupMenuItems = [];
popupMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
));
List<Widget> emptyBody = [
Text("."),
];
if (ConnectionManager().isAuthenticated) {
_showLoginButton = false;
popupMenuItems.add(
PopupMenuItem<String>(
child: new Text("Logout"),
value: "logout",
));
}
if (_showLoginButton) {
emptyBody = [
FlatButton(
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
color: Colors.blue,
onPressed: () => _fullLoad(),
)
];
}
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
floating: true,
pinned: true,
primary: true,
title: Text(HomeAssistant().locationName ?? ""),
actions: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:television"), color: Colors.white,),
onPressed: () => Navigator.pushNamed(context, "/play-media", arguments: {"url": ""})
),
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Colors.white,),
onPressed: () {
showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
context: context,
items: popupMenuItems
).then((String val) {
if (val == "reload") {
_quickLoad();
} else if (val == "logout") {
HomeAssistant().logout().then((_) {
_quickLoad();
});
}
});
}
)
],
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
},
),
bottom: empty ? null : TabBar(
controller: _viewsTabController,
tabs: buildUIViewTabs(),
isScrollable: true,
),
),
];
},
body: empty ?
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: emptyBody
),
)
:
HomeAssistant().buildViews(context, _viewsTabController),
);
}
TabController _viewsTabController;
@override
Widget build(BuildContext context) {
Widget bottomBar;
if (_showBottomBar) {
List<Widget> bottomBarChildren = [];
if (_bottomBarText != null) {
bottomBarChildren.add(
Padding(
padding: EdgeInsets.fromLTRB(
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
Sizes.rowPadding),
child: Text(
"$_bottomBarText",
textAlign: TextAlign.left,
softWrap: true,
),
)
);
}
if (_bottomBarProgress) {
bottomBarChildren.add(
CollectionScaleTransition(
children: <Widget>[
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.on),),
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.unavailable),),
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.off),),
],
),
);
}
if (bottomBarChildren.isNotEmpty) {
bottomBar = Container(
color: _bottomBarColor,
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: bottomBarChildren,
),
),
_bottomBarAction
],
),
);
}
}
// This method is rerun every time setState is called.
if (HomeAssistant().isNoViews) {
return Scaffold(
key: _scaffoldKey,
primary: false,
drawer: _buildAppDrawer(),
bottomNavigationBar: bottomBar,
body: _buildScaffoldBody(true)
);
} else {
return Scaffold(
key: _scaffoldKey,
drawer: _buildAppDrawer(),
primary: false,
bottomNavigationBar: bottomBar,
body: _buildScaffoldBody(false),
);
}
}
@override
void dispose() {
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.dispose();
WidgetsBinding.instance.removeObserver(this);
_viewsTabController?.dispose();
_stateSubscription?.cancel();
_settingsSubscription?.cancel();
_serviceCallSubscription?.cancel();
_showPopupDialogSubscription?.cancel();
_showPopupMessageSubscription?.cancel();
_showEntityPageSubscription?.cancel();
_showErrorSubscription?.cancel();
_startAuthSubscription?.cancel();
_subscription?.cancel();
_showPageSubscription?.cancel();
_reloadUISubscription?.cancel();
//TODO disconnect
//widget.homeAssistant?.disconnect();
super.dispose();
}
}

View File

@ -1,4 +1,4 @@
part of 'main.dart';
part of '../main.dart';
class PanelPage extends StatefulWidget {
PanelPage({Key key, this.title, this.panel}) : super(key: key);
@ -12,8 +12,6 @@ class PanelPage extends StatefulWidget {
class _PanelPageState extends State<PanelPage> {
List<ConfigurationItem> _items;
@override
void initState() {
super.initState();

View File

@ -0,0 +1,225 @@
part of '../main.dart';
class PlayMediaPage extends StatefulWidget {
final String mediaUrl;
PlayMediaPage({Key key, this.mediaUrl}) : super(key: key);
@override
_PlayMediaPageState createState() => new _PlayMediaPageState();
}
class _PlayMediaPageState extends State<PlayMediaPage> {
bool _loaded = false;
String _error = "";
String _validationMessage = "";
List<Entity> _players;
String _mediaUrl;
String _contentType;
bool _useMediaExtractor = false;
bool _isMediaExtractorExist = false;
StreamSubscription _stateSubscription;
StreamSubscription _refreshDataSubscription;
final List<String> _contentTypes = ["movie", "video", "music", "image", "image/jpg", "playlist"];
@override
void initState() {
super.initState();
_mediaUrl = widget.mediaUrl;
_contentType = _contentTypes[0];
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.entityId.contains("media_player")) {
Logger.d("State change event handled by play media page: ${event.entityId}");
setState(() {});
}
});
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
_loadMediaEntities();
});
_loadMediaEntities();
}
_loadMediaEntities() async {
if (HomeAssistant().isNoEntities) {
setState(() {
_loaded = false;
});
} else {
_isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor");
//_useMediaExtractor = _isMediaExtractorExist;
_players = HomeAssistant().entities.getByDomains(["media_player"]);
setState(() {
if (_players.isNotEmpty) {
_loaded = true;
} else {
_loaded = false;
_error = "Looks like you don't have any media player";
}
});
}
}
void _playMedia(Entity entity) {
if (_mediaUrl == null || _mediaUrl.isEmpty) {
setState(() {
_validationMessage = "Media url must be specified";
});
} else {
String serviceDomain;
if (_useMediaExtractor) {
serviceDomain = "media_extractor";
} else {
serviceDomain = "media_player";
}
Navigator.pop(context);
ConnectionManager().callService(
domain: serviceDomain,
entityId: entity.entityId,
service: "play_media",
additionalServiceData: {
"media_content_id": _mediaUrl,
"media_content_type": _contentType
}
);
eventBus.fire(ShowEntityPageEvent(entity));
}
}
@override
Widget build(BuildContext context) {
Widget body;
if (!_loaded) {
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
} else {
List<Widget> children = [];
children.add(CardHeader(name: "Media:"));
children.add(
TextField(
maxLines: 5,
minLines: 1,
decoration: InputDecoration(
labelText: "Media url"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _mediaUrl)),
onChanged: (value) {
_mediaUrl = value;
}
),
);
if (_validationMessage.isNotEmpty) {
children.add(Text(
"$_validationMessage",
style: TextStyle(color: Colors.red)
));
}
children.addAll(<Widget>[
Container(height: Sizes.rowPadding,),
DropdownButton<String>(
value: _contentType,
isExpanded: true,
items: _contentTypes.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (value) {
setState(() {
_contentType = value;
});
},
)
]
);
if (_isMediaExtractorExist) {
children.addAll(<Widget>[
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Text("Use media extractor"),
),
Switch(
value: _useMediaExtractor,
onChanged: (value) => setState((){_useMediaExtractor = value;}),
),
],
),
Container(
height: Sizes.rowPadding,
)
]
);
} else {
children.addAll(<Widget>[
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Text("You can use media extractor here"),
),
GestureDetector(
onTap: () {
Launcher.launchURLInCustomTab(
context: context,
url: "https://www.home-assistant.io/components/media_extractor/"
);
},
child: Text(
"How?",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
),
),
),
],
),
Container(
height: Sizes.doubleRowPadding,
)
]
);
}
children.add(CardHeader(name: "Play on:"));
children.addAll(
_players.map((player) => InkWell(
child: EntityModel(
entityWrapper: EntityWrapper(entity: player),
handleTap: false,
child: Padding(
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
child: DefaultEntityContainer(state: player._buildStatePart(context)),
)
),
onTap: () => _playMedia(player),
))
);
body = ListView(
padding: EdgeInsets.all(Sizes.leftWidgetPadding),
scrollDirection: Axis.vertical,
children: children
);
}
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
title: new Text("Play media"),
),
body: body,
);
}
@override
void dispose(){
_stateSubscription?.cancel();
_refreshDataSubscription?.cancel();
super.dispose();
}
}

View File

@ -0,0 +1,107 @@
part of '../main.dart';
class PurchasePage extends StatefulWidget {
PurchasePage({Key key, this.title}) : super(key: key);
final String title;
@override
_PurchasePageState createState() => new _PurchasePageState();
}
class _PurchasePageState extends State<PurchasePage> {
bool _loaded = false;
String _error = "";
List<ProductDetails> _products;
List<PurchaseDetails> _purchases;
@override
void initState() {
super.initState();
_loadProducts();
}
_loadProducts() async {
final bool available = await InAppPurchaseConnection.instance.isAvailable();
if (!available) {
setState(() {
_error = "Error connecting to store";
});
} else {
const Set<String> _kIds = {'one_time_support','just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
if (!response.notFoundIDs.isEmpty) {
Logger.d("Products not found: ${response.notFoundIDs}");
}
_products = response.productDetails;
_loadPreviousPurchases();
}
}
_loadPreviousPurchases() async {
final QueryPurchaseDetailsResponse response = await InAppPurchaseConnection.instance.queryPastPurchases();
if (response.error != null) {
setState(() {
_error = "Error loading previous purchases";
});
} else {
_purchases = response.pastPurchases;
for (PurchaseDetails purchase in _purchases) {
Logger.d("Previous purchase: ${purchase.status}");
}
if (_products.isEmpty) {
setState(() {
_error = "No data found in store";
});
} else {
setState(() {
_loaded = true;
});
}
}
}
Widget _buildProducts() {
List<Widget> productWidgets = [];
for (ProductDetails product in _products) {
productWidgets.add(
ProductPurchase(
product: product,
onBuy: (product) => _buyProduct(product),
purchased: _purchases.any((purchase) { return purchase.productID == product.id;}),)
);
}
return ListView(
scrollDirection: Axis.vertical,
children: productWidgets
);
}
void _buyProduct(ProductDetails product) {
Logger.d("Starting purchase of ${product.id}");
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
InAppPurchaseConnection.instance.buyNonConsumable(purchaseParam: purchaseParam);
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
Widget body;
if (!_loaded) {
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
} else {
body = _buildProducts();
}
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
title: new Text(widget.title),
),
body: body,
);
}
}

View File

@ -1,4 +1,4 @@
part of 'main.dart';
part of '../main.dart';
class ConnectionSettingsPage extends StatefulWidget {
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
@ -16,10 +16,13 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _newHassioPort = "";
String _socketProtocol = "wss";
String _newSocketProtocol = "wss";
String _longLivedToken = "";
String _newLongLivedToken = "";
bool _useLovelace = true;
bool _newUseLovelace = true;
String oauthUrl;
bool useOAuth = false;
@override
void initState() {
@ -30,6 +33,23 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
_loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage();
try {
useOAuth = prefs.getBool("oauth-used") ?? true;
} catch (e) {
useOAuth = true;
}
if (!useOAuth) {
try {
_longLivedToken = _newLongLivedToken =
await storage.read(key: "hacl_llt");
} catch (e) {
_longLivedToken = _newLongLivedToken = "";
await storage.delete(key: "hacl_llt");
}
}
setState(() {
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
@ -48,16 +68,32 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
(_newUseLovelace != _useLovelace));
(_newUseLovelace != _useLovelace) ||
(_newLongLivedToken != _longLivedToken));
}
_saveSettings() async {
_newHassioDomain = _newHassioDomain.trim();
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
_newHassioDomain = _newHassioDomain.split("//")[1];
}
_newHassioDomain = _newHassioDomain.split("/")[0];
SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage();
if (_newLongLivedToken.isNotEmpty) {
_newLongLivedToken = _newLongLivedToken.trim();
prefs.setBool("oauth-used", false);
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
} else if (!useOAuth) {
await storage.delete(key: "hacl_llt");
}
prefs.setString("hassio-domain", _newHassioDomain);
if (_newHassioPort == null || _newHassioPort.isEmpty) {
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
} else {
_newHassioPort = _newHassioPort.trim();
}
prefs.setString("hassio-port", _newHassioPort);
prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
@ -118,13 +154,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
decoration: InputDecoration(
labelText: "Home Assistant domain or ip address"
),
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _newHassioDomain,
selection:
new TextSelection.collapsed(offset: _newHassioDomain.length)
)
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
onChanged: (value) {
_newHassioDomain = value;
}
@ -133,13 +163,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
decoration: InputDecoration(
labelText: "Home Assistant port (default is 8123)"
),
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _newHassioPort,
selection:
new TextSelection.collapsed(offset: _newHassioPort.length)
)
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
onChanged: (value) {
_newHassioPort = value;
}
@ -171,6 +195,27 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
)
],
),
Text(
"Authentication settings",
style: TextStyle(
color: Colors.black45,
fontSize: 20.0
),
),
Container(height: 10.0,),
Text(
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
style: TextStyle(color: Colors.redAccent),
),
new TextField(
decoration: InputDecoration(
labelText: "Long-lived token"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
onChanged: (value) {
_newLongLivedToken = value;
}
),
],
),
);

View File

@ -0,0 +1,32 @@
part of '../../main.dart';
class PageLoadingError extends StatelessWidget {
final String errorText;
const PageLoadingError({Key key, this.errorText: "Error"}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Column(
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
child: Icon(
Icons.error,
color: Colors.redAccent,
size: 48.0
)
),
Text(this.errorText, style: TextStyle(color: Colors.black45))
],
)
],
);
}
}

View File

@ -0,0 +1,23 @@
part of '../../main.dart';
class PageLoadingIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Column(
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
child: CircularProgressIndicator()
),
Text("Loading...", style: TextStyle(color: Colors.black45))
],
)
],
);
}
}

View File

@ -0,0 +1,74 @@
part of '../../main.dart';
class ProductPurchase extends StatelessWidget {
final ProductDetails product;
final onBuy;
final purchased;
const ProductPurchase({Key key, @required this.product, @required this.onBuy, this.purchased}) : super(key: key);
@override
Widget build(BuildContext context) {
String period = "";
Color priceColor;
String buttonText = '';
String buttonTextInactive = '';
if (product.id.contains("year")) {
period += "/ year";
buttonText = "Subscribe";
buttonTextInactive = "Already";
priceColor = Colors.amber;
} else {
period += "";
buttonText = "Pay";
buttonTextInactive = "Paid";
priceColor = Colors.deepOrangeAccent;
}
return Card(
child: Padding(
padding: EdgeInsets.all(Sizes.leftWidgetPadding),
child: Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
flex: 5,
child: Padding(
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
"${product.title}",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16.0
),
),
Container(height: Sizes.rowPadding,),
Text(
"${product.description}",
overflow: TextOverflow.ellipsis,
maxLines: 4,
softWrap: true,
),
Container(height: Sizes.rowPadding,),
Text("${product.price} $period", style: TextStyle(color: priceColor)),
],
)
),
),
Expanded(
flex: 2,
child: RaisedButton(
child: Text(this.purchased ? buttonTextInactive : buttonText, style: TextStyle(color: Colors.white)),
color: Colors.blue,
onPressed: this.purchased ? null : () => this.onBuy(this.product),
),
)
],
),
)
);
}
}

View File

@ -0,0 +1,120 @@
part of '../main.dart';
class ConfigPanelWidget extends StatefulWidget {
ConfigPanelWidget({Key key}) : super(key: key);
@override
_ConfigPanelWidgetState createState() => new _ConfigPanelWidgetState();
}
class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
@override
void initState() {
super.initState();
}
restart() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you want to restart Home Assistant?",
body: "This will restart your Home Assistant server.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "restart", entityId: null);
},
));
}
stop() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you wanr to STOP Home Assistant?",
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "stop", entityId: null);
},
));
}
updateRegistration() {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
}
resetRegistration() {
eventBus.fire(ShowPopupDialogEvent(
title: "Waaaait",
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
positiveText: "Done it already",
negativeText: "Ok, I will",
onPositive: () {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
},
));
}
@override
Widget build(BuildContext context) {
return ListView(
children: [
Card(
child: Padding(
padding: EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ListTile(
title: Text("Mobile app integration",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize))
),
Text("Registration", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
Container(height: Sizes.rowPadding,),
Text("${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}"),
Container(height: 6.0,),
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
//Divider(),
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
RaisedButton(
color: Colors.blue,
onPressed: () => updateRegistration(),
child: Text("Check registration", style: TextStyle(color: Colors.white))
),
Container(width: 10.0,),
RaisedButton(
color: Colors.redAccent,
onPressed: () => resetRegistration(),
child: Text("Reset registration", style: TextStyle(color: Colors.white))
)
],
),
],
),
),
),
LinkToWebConfig(name: "Home Assistant Cloud", url: ConnectionManager().httpWebHost+"/config/cloud/account"),
Container(height: 8.0,),
LinkToWebConfig(name: "Integrations", url: ConnectionManager().httpWebHost+"/config/integrations/dashboard"),
LinkToWebConfig(name: "Users", url: ConnectionManager().httpWebHost+"/config/users/picker"),
Container(height: 8.0,),
LinkToWebConfig(name: "General", url: ConnectionManager().httpWebHost+"/config/core"),
LinkToWebConfig(name: "Server Control", url: ConnectionManager().httpWebHost+"/config/server_control"),
LinkToWebConfig(name: "Persons", url: ConnectionManager().httpWebHost+"/config/person"),
LinkToWebConfig(name: "Entity Registry", url: ConnectionManager().httpWebHost+"/config/entity_registry"),
LinkToWebConfig(name: "Area Registry", url: ConnectionManager().httpWebHost+"/config/area_registry"),
LinkToWebConfig(name: "Automation", url: ConnectionManager().httpWebHost+"/config/automation"),
LinkToWebConfig(name: "Script", url: ConnectionManager().httpWebHost+"/config/script"),
LinkToWebConfig(name: "Customization", url: ConnectionManager().httpWebHost+"/config/customize"),
],
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -17,29 +17,25 @@ class Panel {
final Map config;
String icon;
bool isHidden = true;
bool isWebView = false;
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
if (icon == null || !icon.startsWith("mdi:")) {
icon = Panel.iconsByComponent[type];
}
isHidden = (type != "iframe" && type != "config");
isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools');
isWebView = (type != 'config');
}
void handleOpen(BuildContext context) {
if (type == "iframe") {
Logger.d("Launching custom tab with ${config["url"]}");
HAUtils.launchURLInCustomTab(context, config["url"]);
} else if (type == "config") {
if (type == "config") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PanelPage(title: "$title", panel: this),
)
);
} else {
HomeAssistantModel haModel = HomeAssistantModel.of(context);
String url = "${haModel.homeAssistant.connection.httpWebHost}/$urlPath";
Logger.d("Launching custom tab with $url");
HAUtils.launchURLInCustomTab(context, url);
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}");
}
}

View File

@ -0,0 +1,29 @@
part of '../../main.dart';
class LinkToWebConfig extends StatelessWidget {
final String name;
final String url;
const LinkToWebConfig({Key key, @required this.name, @required this.url}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: <Widget>[
ListTile(
title: Text("${this.name}",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
subtitle: Text("Tap to open web version"),
onTap: () {
Launcher.launchAuthenticatedWebView(context: context, url: this.url, title: this.name);
},
)
],
),
);
}
}

View File

@ -0,0 +1,77 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'utils.dart';
class BasePainter extends CustomPainter {
Color baseColor;
Color selectionColor;
int primarySectors;
int secondarySectors;
double sliderStrokeWidth;
Offset center;
double radius;
BasePainter({
@required this.baseColor,
@required this.selectionColor,
@required this.primarySectors,
@required this.secondarySectors,
@required this.sliderStrokeWidth,
});
@override
void paint(Canvas canvas, Size size) {
Paint base = _getPaint(color: baseColor);
center = Offset(size.width / 2, size.height / 2);
radius = min(size.width / 2, size.height / 2) - sliderStrokeWidth;
// we need this in the parent to calculate if the user clicks on the circumference
assert(radius > 0);
canvas.drawCircle(center, radius, base);
if (primarySectors > 0) {
_paintSectors(primarySectors, 8.0, selectionColor, canvas);
}
if (secondarySectors > 0) {
_paintSectors(secondarySectors, 6.0, baseColor, canvas);
}
}
void _paintSectors(
int sectors, double radiusPadding, Color color, Canvas canvas) {
Paint section = _getPaint(color: color, width: 2.0);
var endSectors =
getSectionsCoordinatesInCircle(center, radius + radiusPadding, sectors);
var initSectors =
getSectionsCoordinatesInCircle(center, radius - radiusPadding, sectors);
_paintLines(canvas, initSectors, endSectors, section);
}
void _paintLines(
Canvas canvas, List<Offset> inits, List<Offset> ends, Paint section) {
assert(inits.length == ends.length && inits.length > 0);
for (var i = 0; i < inits.length; i++) {
canvas.drawLine(inits[i], ends[i], section);
}
}
Paint _getPaint({@required Color color, double width, PaintingStyle style}) =>
Paint()
..color = color
..strokeCap = StrokeCap.round
..style = style ?? PaintingStyle.stroke
..strokeWidth = width ?? sliderStrokeWidth;
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}

View File

@ -0,0 +1,366 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'base_painter.dart';
import 'slider_painter.dart';
import 'utils.dart';
enum CircularSliderMode { singleHandler, doubleHandler }
enum SlidingState { none, endIsBiggerThanStart, endIsSmallerThanStart }
typedef SelectionChanged<T> = void Function(T a, T b, T c);
class CircularSliderPaint extends StatefulWidget {
final CircularSliderMode mode;
final int init;
final int end;
final int divisions;
final int primarySectors;
final int secondarySectors;
final SelectionChanged<int> onSelectionChange;
final SelectionChanged<int> onSelectionEnd;
final Color baseColor;
final Color selectionColor;
final Color handlerColor;
final double handlerOutterRadius;
final Widget child;
final bool showRoundedCapInSelection;
final bool showHandlerOutter;
final double sliderStrokeWidth;
final bool shouldCountLaps;
CircularSliderPaint({
@required this.mode,
@required this.divisions,
@required this.init,
@required this.end,
this.child,
@required this.primarySectors,
@required this.secondarySectors,
@required this.onSelectionChange,
@required this.onSelectionEnd,
@required this.baseColor,
@required this.selectionColor,
@required this.handlerColor,
@required this.handlerOutterRadius,
@required this.showRoundedCapInSelection,
@required this.showHandlerOutter,
@required this.sliderStrokeWidth,
@required this.shouldCountLaps,
});
@override
_CircularSliderState createState() => _CircularSliderState();
}
class _CircularSliderState extends State<CircularSliderPaint> {
bool _isInitHandlerSelected = false;
bool _isEndHandlerSelected = false;
SliderPainter _painter;
/// start angle in radians where we need to locate the init handler
double _startAngle;
/// end angle in radians where we need to locate the end handler
double _endAngle;
/// the absolute angle in radians representing the selection
double _sweepAngle;
/// in case we have a double slider and we want to move the whole selection by clicking in the slider
/// this will capture the position in the selection relative to the initial handler
/// that way we will be able to keep the selection constant when moving
int _differenceFromInitPoint;
/// will store the number of full laps (2pi radians) as part of the selection
int _laps = 0;
/// will be used to calculate in the next movement if we need to increase or decrease _laps
SlidingState _slidingState = SlidingState.none;
bool get isDoubleHandler => widget.mode == CircularSliderMode.doubleHandler;
bool get isSingleHandler => widget.mode == CircularSliderMode.singleHandler;
bool get isBothHandlersSelected =>
_isEndHandlerSelected && _isInitHandlerSelected;
bool get isNoHandlersSelected =>
!_isEndHandlerSelected && !_isInitHandlerSelected;
@override
void initState() {
super.initState();
_calculatePaintData();
}
// we need to update this widget both with gesture detector but
// also when the parent widget rebuilds itself
@override
void didUpdateWidget(CircularSliderPaint oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.init != widget.init || oldWidget.end != widget.end) {
_calculatePaintData();
}
}
@override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
CustomPanGestureRecognizer:
GestureRecognizerFactoryWithHandlers<CustomPanGestureRecognizer>(
() => CustomPanGestureRecognizer(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
),
(CustomPanGestureRecognizer instance) {},
),
},
child: CustomPaint(
painter: BasePainter(
baseColor: widget.baseColor,
selectionColor: widget.selectionColor,
primarySectors: widget.primarySectors,
secondarySectors: widget.secondarySectors,
sliderStrokeWidth: widget.sliderStrokeWidth,
),
foregroundPainter: _painter,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: widget.child,
),
),
);
}
void _calculatePaintData() {
var initPercent = isDoubleHandler
? valueToPercentage(widget.init, widget.divisions)
: 0.0;
var endPercent = valueToPercentage(widget.end, widget.divisions);
var sweep = getSweepAngle(initPercent, endPercent);
var previousStartAngle = _startAngle;
var previousEndAngle = _endAngle;
_startAngle = isDoubleHandler ? percentageToRadians(initPercent) : 0.0;
_endAngle = percentageToRadians(endPercent);
_sweepAngle = percentageToRadians(sweep.abs());
// update full laps if need be
if (widget.shouldCountLaps) {
var newSlidingState = _calculateSlidingState(_startAngle, _endAngle);
if (isSingleHandler) {
_laps = _calculateLapsForsSingleHandler(
_endAngle, previousEndAngle, _slidingState, _laps);
_slidingState = newSlidingState;
} else {
// is double handler
if (newSlidingState != _slidingState) {
_laps = _calculateLapsForDoubleHandler(
_startAngle,
_endAngle,
previousStartAngle,
previousEndAngle,
_slidingState,
newSlidingState,
_laps);
_slidingState = newSlidingState;
}
}
}
_painter = SliderPainter(
mode: widget.mode,
startAngle: _startAngle,
endAngle: _endAngle,
sweepAngle: _sweepAngle,
selectionColor: widget.selectionColor,
handlerColor: widget.handlerColor,
handlerOutterRadius: widget.handlerOutterRadius,
showRoundedCapInSelection: widget.showRoundedCapInSelection,
showHandlerOutter: widget.showHandlerOutter,
sliderStrokeWidth: widget.sliderStrokeWidth,
);
}
int _calculateLapsForsSingleHandler(
double end, double prevEnd, SlidingState slidingState, int laps) {
if (slidingState != SlidingState.none) {
if (radiansWasModuloed(end, prevEnd)) {
var lapIncrement = end < prevEnd ? 1 : -1;
var newLaps = laps + lapIncrement;
return newLaps < 0 ? 0 : newLaps;
}
}
return laps;
}
int _calculateLapsForDoubleHandler(
double start,
double end,
double prevStart,
double prevEnd,
SlidingState slidingState,
SlidingState newSlidingState,
int laps) {
if (slidingState != SlidingState.none) {
if (!radiansWasModuloed(start, prevStart) &&
!radiansWasModuloed(end, prevEnd)) {
var lapIncrement =
newSlidingState == SlidingState.endIsBiggerThanStart ? 1 : -1;
var newLaps = laps + lapIncrement;
return newLaps < 0 ? 0 : newLaps;
}
}
return laps;
}
SlidingState _calculateSlidingState(double start, double end) {
return end > start
? SlidingState.endIsBiggerThanStart
: SlidingState.endIsSmallerThanStart;
}
void _onPanUpdate(Offset details) {
if (!_isInitHandlerSelected && !_isEndHandlerSelected) {
return;
}
if (_painter.center == null) {
return;
}
_handlePan(details, false);
}
void _onPanEnd(Offset details) {
_handlePan(details, true);
_isInitHandlerSelected = false;
_isEndHandlerSelected = false;
}
void _handlePan(Offset details, bool isPanEnd) {
RenderBox renderBox = context.findRenderObject();
var position = renderBox.globalToLocal(details);
var angle = coordinatesToRadians(_painter.center, position);
var percentage = radiansToPercentage(angle);
var newValue = percentageToValue(percentage, widget.divisions);
if (isBothHandlersSelected) {
var newValueInit =
(newValue - _differenceFromInitPoint) % widget.divisions;
if (newValueInit != widget.init) {
var newValueEnd =
(widget.end + (newValueInit - widget.init)) % widget.divisions;
widget.onSelectionChange(newValueInit, newValueEnd, _laps);
if (isPanEnd) {
widget.onSelectionEnd(newValueInit, newValueEnd, _laps);
}
}
return;
}
// isDoubleHandler but one handler was selected
if (_isInitHandlerSelected) {
widget.onSelectionChange(newValue, widget.end, _laps);
if (isPanEnd) {
widget.onSelectionEnd(newValue, widget.end, _laps);
}
} else {
widget.onSelectionChange(widget.init, newValue, _laps);
if (isPanEnd) {
widget.onSelectionEnd(widget.init, newValue, _laps);
}
}
}
bool _onPanDown(Offset details) {
if (_painter == null) {
return false;
}
RenderBox renderBox = context.findRenderObject();
var position = renderBox.globalToLocal(details);
if (position == null) {
return false;
}
if (isSingleHandler) {
if (isPointAlongCircle(position, _painter.center, _painter.radius)) {
_isEndHandlerSelected = true;
_onPanUpdate(details);
}
} else {
_isInitHandlerSelected = isPointInsideCircle(
position, _painter.initHandler, widget.handlerOutterRadius);
if (!_isInitHandlerSelected) {
_isEndHandlerSelected = isPointInsideCircle(
position, _painter.endHandler, widget.handlerOutterRadius);
}
if (isNoHandlersSelected) {
// we check if the user pressed in the selection in a double handler slider
// that means the user wants to move the selection as a whole
if (isPointAlongCircle(position, _painter.center, _painter.radius)) {
var angle = coordinatesToRadians(_painter.center, position);
if (isAngleInsideRadiansSelection(angle, _startAngle, _sweepAngle)) {
_isEndHandlerSelected = true;
_isInitHandlerSelected = true;
var positionPercentage = radiansToPercentage(angle);
// no need to account for negative values, that will be sorted out in the onPanUpdate
_differenceFromInitPoint =
percentageToValue(positionPercentage, widget.divisions) -
widget.init;
}
}
}
}
return _isInitHandlerSelected || _isEndHandlerSelected;
}
}
class CustomPanGestureRecognizer extends OneSequenceGestureRecognizer {
final Function onPanDown;
final Function onPanUpdate;
final Function onPanEnd;
CustomPanGestureRecognizer({
@required this.onPanDown,
@required this.onPanUpdate,
@required this.onPanEnd,
});
@override
void addPointer(PointerEvent event) {
if (onPanDown(event.position)) {
startTrackingPointer(event.pointer);
resolve(GestureDisposition.accepted);
} else {
stopTrackingPointer(event.pointer);
}
}
@override
void handleEvent(PointerEvent event) {
if (event is PointerMoveEvent) {
onPanUpdate(event.position);
}
if (event is PointerUpEvent) {
onPanEnd(event.position);
stopTrackingPointer(event.pointer);
}
}
@override
String get debugDescription => 'customPan';
@override
void didStopTrackingLastPointer(int pointer) {}
}

View File

@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'circular_slider_paint.dart';
/// Returns a widget which displays a circle to be used as a slider.
///
/// Required arguments are init and end to set the initial selection.
/// onSelectionChange is a callback function which returns new values as the user
/// changes the interval.
/// The rest of the params are used to change the look and feel.
///
/// DoubleCircularSlider(5, 10, onSelectionChange: () => {});
class DoubleCircularSlider extends StatefulWidget {
/// the selection will be values between 0..divisions; max value is 300
final int divisions;
/// the initial value in the selection
final int init;
/// the end value in the selection
final int end;
/// the number of primary sectors to be painted
/// will be painted using selectionColor
final int primarySectors;
/// the number of secondary sectors to be painted
/// will be painted using baseColor
final int secondarySectors;
/// an optional widget that would be mounted inside the circle
final Widget child;
/// height of the canvas, default at 220
final double height;
/// width of the canvas, default at 220
final double width;
/// color of the base circle and sections
final Color baseColor;
/// color of the selection
final Color selectionColor;
/// color of the handlers
final Color handlerColor;
/// callback function when init and end change
/// (int init, int end) => void
final SelectionChanged<int> onSelectionChange;
/// callback function when init and end finish
/// (int init, int end) => void
final SelectionChanged<int> onSelectionEnd;
/// outter radius for the handlers
final double handlerOutterRadius;
/// if true an extra handler ring will be displayed in the handler
final bool showHandlerOutter;
/// stroke width for the slider, defaults at 12.0
final double sliderStrokeWidth;
/// if true, the onSelectionChange will also return the number of laps in the slider
/// otherwise, everytime the user completes a full lap, the selection restarts from 0
final bool shouldCountLaps;
DoubleCircularSlider(
this.divisions,
this.init,
this.end, {
this.height,
this.width,
this.child,
this.primarySectors,
this.secondarySectors,
this.baseColor,
this.selectionColor,
this.handlerColor,
this.onSelectionChange,
this.onSelectionEnd,
this.handlerOutterRadius,
this.showHandlerOutter,
this.sliderStrokeWidth,
this.shouldCountLaps,
}) : assert(init >= 0 && init <= divisions,
'init has to be > 0 and < divisions value'),
assert(end >= 0 && end <= divisions,
'end has to be > 0 and < divisions value'),
assert(divisions >= 0 && divisions <= 300,
'divisions has to be > 0 and <= 300');
@override
_DoubleCircularSliderState createState() => _DoubleCircularSliderState();
}
class _DoubleCircularSliderState extends State<DoubleCircularSlider> {
int _init;
int _end;
@override
void initState() {
super.initState();
_init = widget.init;
_end = widget.end;
}
@override
Widget build(BuildContext context) {
return Container(
height: widget.height ?? 220,
width: widget.width ?? 220,
child: CircularSliderPaint(
mode: CircularSliderMode.doubleHandler,
init: _init,
end: _end,
divisions: widget.divisions,
primarySectors: widget.primarySectors ?? 0,
secondarySectors: widget.secondarySectors ?? 0,
child: widget.child,
onSelectionChange: (newInit, newEnd, laps) {
if (widget.onSelectionChange != null) {
widget.onSelectionChange(newInit, newEnd, laps);
}
setState(() {
_init = newInit;
_end = newEnd;
});
},
onSelectionEnd: (newInit, newEnd, laps) {
if (widget.onSelectionEnd != null) {
widget.onSelectionEnd(newInit, newEnd, laps);
}
},
sliderStrokeWidth: widget.sliderStrokeWidth ?? 12.0,
baseColor: widget.baseColor ?? Color.fromRGBO(255, 255, 255, 0.1),
selectionColor:
widget.selectionColor ?? Color.fromRGBO(255, 255, 255, 0.3),
handlerColor: widget.handlerColor ?? Colors.white,
handlerOutterRadius: widget.handlerOutterRadius ?? 12.0,
showRoundedCapInSelection: false,
showHandlerOutter: widget.showHandlerOutter ?? true,
shouldCountLaps: widget.shouldCountLaps ?? false,
));
}
}

View File

@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'circular_slider_paint.dart';
import '../../utils/logger.dart';
/// Returns a widget which displays a circle to be used as a slider.
///
/// Required arguments are position and divisions to set the initial selection.
/// onSelectionChange is a callback function which returns new values as the user
/// changes the interval.
/// The rest of the params are used to change the look and feel.
///
/// SingleCircularSlider(5, 10, onSelectionChange: () => {});
class SingleCircularSlider extends StatefulWidget {
/// the selection will be values between 0..divisions; max value is 300
final int divisions;
/// the initial value in the selection
int position;
/// the number of primary sectors to be painted
/// will be painted using selectionColor
final int primarySectors;
/// the number of secondary sectors to be painted
/// will be painted using baseColor
final int secondarySectors;
/// an optional widget that would be mounted inside the circle
final Widget child;
/// height of the canvas, default at 220
final double height;
/// width of the canvas, default at 220
final double width;
/// color of the base circle and sections
final Color baseColor;
/// color of the selection
final Color selectionColor;
/// color of the handlers
final Color handlerColor;
/// callback function when init and end change
/// (int init, int end) => void
final SelectionChanged<int> onSelectionChange;
/// callback function when init and end finish
/// (int init, int end) => void
final SelectionChanged<int> onSelectionEnd;
/// outter radius for the handlers
final double handlerOutterRadius;
/// if true will paint a rounded cap in the selection slider start
final bool showRoundedCapInSelection;
/// if true an extra handler ring will be displayed in the handler
final bool showHandlerOutter;
/// stroke width for the slider, defaults at 12.0
final double sliderStrokeWidth;
/// if true, the onSelectionChange will also return the number of laps in the slider
/// otherwise, everytime the user completes a full lap, the selection restarts from 0
final bool shouldCountLaps;
SingleCircularSlider(
this.divisions,
this.position, {
this.height,
this.width,
this.child,
this.primarySectors,
this.secondarySectors,
this.baseColor,
this.selectionColor,
this.handlerColor,
this.onSelectionChange,
this.onSelectionEnd,
this.handlerOutterRadius,
this.showRoundedCapInSelection,
this.showHandlerOutter,
this.sliderStrokeWidth,
this.shouldCountLaps,
}) : assert(position >= 0 && position <= divisions,
'init has to be > 0 and < divisions value'),
assert(divisions >= 0 && divisions <= 300,
'divisions has to be > 0 and <= 300');
@override
_SingleCircularSliderState createState() => _SingleCircularSliderState();
}
class _SingleCircularSliderState extends State<SingleCircularSlider> {
int _end;
@override
void initState() {
super.initState();
_end = widget.position;
Logger.d('Init: _end=$_end');
}
@override
Widget build(BuildContext context) {
Logger.d('Build: _end=$_end');
return Container(
height: widget.height ?? 220,
width: widget.width ?? 220,
child: CircularSliderPaint(
mode: CircularSliderMode.singleHandler,
init: 0,
end: _end,
divisions: widget.divisions,
primarySectors: widget.primarySectors ?? 0,
secondarySectors: widget.secondarySectors ?? 0,
child: widget.child,
onSelectionChange: (newInit, newEnd, laps) {
if (widget.onSelectionChange != null) {
widget.onSelectionChange(newInit, newEnd, laps);
}
setState(() {
_end = newEnd;
});
},
onSelectionEnd: (newInit, newEnd, laps) {
if (widget.onSelectionEnd != null) {
widget.onSelectionEnd(newInit, newEnd, laps);
}
},
sliderStrokeWidth: widget.sliderStrokeWidth ?? 12.0,
baseColor: widget.baseColor ?? Color.fromRGBO(255, 255, 255, 0.1),
selectionColor:
widget.selectionColor ?? Color.fromRGBO(255, 255, 255, 0.3),
handlerColor: widget.handlerColor ?? Colors.white,
handlerOutterRadius: widget.handlerOutterRadius ?? 12.0,
showRoundedCapInSelection: widget.showRoundedCapInSelection ?? false,
showHandlerOutter: widget.showHandlerOutter ?? true,
shouldCountLaps: widget.shouldCountLaps ?? false,
));
}
}

View File

@ -0,0 +1,77 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'circular_slider_paint.dart' show CircularSliderMode;
import 'utils.dart';
class SliderPainter extends CustomPainter {
CircularSliderMode mode;
double startAngle;
double endAngle;
double sweepAngle;
Color selectionColor;
Color handlerColor;
double handlerOutterRadius;
bool showRoundedCapInSelection;
bool showHandlerOutter;
double sliderStrokeWidth;
Offset initHandler;
Offset endHandler;
Offset center;
double radius;
SliderPainter({
@required this.mode,
@required this.startAngle,
@required this.endAngle,
@required this.sweepAngle,
@required this.selectionColor,
@required this.handlerColor,
@required this.handlerOutterRadius,
@required this.showRoundedCapInSelection,
@required this.showHandlerOutter,
@required this.sliderStrokeWidth,
});
@override
void paint(Canvas canvas, Size size) {
Paint progress = _getPaint(color: selectionColor);
center = Offset(size.width / 2, size.height / 2);
radius = min(size.width / 2, size.height / 2) - sliderStrokeWidth;
canvas.drawArc(Rect.fromCircle(center: center, radius: radius),
-pi / 2 + startAngle, sweepAngle, false, progress);
Paint handler = _getPaint(color: handlerColor, style: PaintingStyle.fill);
Paint handlerOutter = _getPaint(color: handlerColor, width: 2.0);
// draw handlers
if (mode == CircularSliderMode.doubleHandler) {
initHandler = radiansToCoordinates(center, -pi / 2 + startAngle, radius);
canvas.drawCircle(initHandler, 8.0, handler);
canvas.drawCircle(initHandler, handlerOutterRadius, handlerOutter);
}
endHandler = radiansToCoordinates(center, -pi / 2 + endAngle, radius);
canvas.drawCircle(endHandler, 8.0, handler);
if (showHandlerOutter) {
canvas.drawCircle(endHandler, handlerOutterRadius, handlerOutter);
}
}
Paint _getPaint({@required Color color, double width, PaintingStyle style}) =>
Paint()
..color = color
..strokeCap =
showRoundedCapInSelection ? StrokeCap.round : StrokeCap.butt
..style = style ?? PaintingStyle.stroke
..strokeWidth = width ?? sliderStrokeWidth;
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}

View File

@ -0,0 +1,75 @@
import 'dart:math';
import 'dart:ui';
double percentageToRadians(double percentage) => ((2 * pi * percentage) / 100);
double radiansToPercentage(double radians) {
var normalized = radians < 0 ? -radians : 2 * pi - radians;
var percentage = ((100 * normalized) / (2 * pi));
// TODO we have an inconsistency of pi/2 in terms of percentage and radians
return (percentage + 25) % 100;
}
double coordinatesToRadians(Offset center, Offset coords) {
var a = coords.dx - center.dx;
var b = center.dy - coords.dy;
return atan2(b, a);
}
Offset radiansToCoordinates(Offset center, double radians, double radius) {
var dx = center.dx + radius * cos(radians);
var dy = center.dy + radius * sin(radians);
return Offset(dx, dy);
}
double valueToPercentage(int time, int intervals) => (time / intervals) * 100;
int percentageToValue(double percentage, int intervals) =>
((percentage * intervals) / 100).round();
bool isPointInsideCircle(Offset point, Offset center, double rradius) {
var radius = rradius * 1.2;
return point.dx < (center.dx + radius) &&
point.dx > (center.dx - radius) &&
point.dy < (center.dy + radius) &&
point.dy > (center.dy - radius);
}
bool isPointAlongCircle(Offset point, Offset center, double radius) {
// distance is root(sqr(x2 - x1) + sqr(y2 - y1))
// i.e., (7,8) and (3,2) -> 7.21
var d1 = pow(point.dx - center.dx, 2);
var d2 = pow(point.dy - center.dy, 2);
var distance = sqrt(d1 + d2);
return (distance - radius).abs() < 10;
}
double getSweepAngle(double init, double end) {
if (end > init) {
return end - init;
}
return (100 - init + end).abs();
}
List<Offset> getSectionsCoordinatesInCircle(
Offset center, double radius, int sections) {
var intervalAngle = (pi * 2) / sections;
return List<int>.generate(sections, (int index) => index).map((i) {
var radians = (pi / 2) + (intervalAngle * i);
return radiansToCoordinates(center, radians, radius);
}).toList();
}
bool isAngleInsideRadiansSelection(double angle, double start, double sweep) {
var normalized = angle > pi / 2 ? 5 * pi / 2 - angle : pi / 2 - angle;
var end = (start + sweep) % (2 * pi);
return end > start
? normalized > start && normalized < end
: normalized > start || normalized < end;
}
// this is not 100% accurate but it works
// we just want to see if a value changed drastically its value
bool radiansWasModuloed(double current, double previous) {
return (previous - current).abs() > (3 * pi / 2);
}

View File

@ -0,0 +1,82 @@
part of '../main.dart';
class StateChangedEvent {
String entityId;
String newState;
bool needToRebuildUI;
StateChangedEvent({
this.entityId,
this.newState,
this.needToRebuildUI: false
});
}
class SettingsChangedEvent {
bool reconnect;
SettingsChangedEvent(this.reconnect);
}
class RefreshDataFinishedEvent {
RefreshDataFinishedEvent();
}
class ReloadUIEvent {
ReloadUIEvent();
}
class StartAuthEvent {
String oauthUrl;
bool showButton;
StartAuthEvent(this.oauthUrl, this.showButton);
}
class ServiceCallEvent {
String domain;
String service;
String entityId;
Map<String, dynamic> additionalParams;
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
}
class ShowPopupDialogEvent {
final String title;
final String body;
final String positiveText;
final String negativeText;
final onPositive;
final onNegative;
ShowPopupDialogEvent({this.title, this.body, this.positiveText: "Ok", this.negativeText: "Cancel", this.onPositive, this.onNegative});
}
class ShowPopupMessageEvent {
final String title;
final String body;
final String buttonText;
final onButtonClick;
ShowPopupMessageEvent({this.title, this.body, this.buttonText: "Ok", this.onButtonClick});
}
class ShowEntityPageEvent {
Entity entity;
ShowEntityPageEvent(this.entity);
}
class ShowPageEvent {
final String path;
final bool goBackFirst;
ShowPageEvent({@required this.path, this.goBackFirst: false});
}
class ShowErrorEvent {
final HAError error;
ShowErrorEvent(this.error);
}

45
lib/types/ha_error.dart Normal file
View File

@ -0,0 +1,45 @@
part of '../main.dart';
class HAError {
String message;
final List<HAErrorAction> actions;
HAError(this.message, {this.actions: const [HAErrorAction.tryAgain()]});
HAError.unableToConnect({this.actions = const [HAErrorAction.tryAgain()]}) {
this.message = "Unable to connect to Home Assistant";
}
HAError.disconnected({this.actions = const [HAErrorAction.reconnect()]}) {
this.message = "Disconnected";
}
HAError.checkConnectionSettings({this.actions = const [HAErrorAction.reload(), HAErrorAction(title: "Settings", type: HAErrorActionType.OPEN_CONNECTION_SETTINGS)]}) {
this.message = "Check connection settings";
}
}
class HAErrorAction {
final String title;
final int type;
final String url;
const HAErrorAction({@required this.title, this.type: HAErrorActionType.FULL_RELOAD, this.url});
const HAErrorAction.tryAgain({this.title = "Try again", this.type = HAErrorActionType.FULL_RELOAD, this.url});
const HAErrorAction.reconnect({this.title = "Reconnect", this.type = HAErrorActionType.FULL_RELOAD, this.url});
const HAErrorAction.reload({this.title = "Reload", this.type = HAErrorActionType.FULL_RELOAD, this.url});
const HAErrorAction.loginAgain({this.title = "Login again", this.type = HAErrorActionType.RELOGIN, this.url});
}
class HAErrorActionType {
static const FULL_RELOAD = 0;
static const QUICK_RELOAD = 1;
static const URL = 3;
static const OPEN_CONNECTION_SETTINGS = 4;
static const RELOGIN = 5;
}

Some files were not shown because too many files have changed in this diff Show More