Compare commits

...

71 Commits

Author SHA1 Message Date
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
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
83 changed files with 3437 additions and 1648 deletions

1
.gitignore vendored
View File

@ -11,4 +11,5 @@ build/
.idea/
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

@ -7,6 +7,10 @@
-->
<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.
@ -14,7 +18,7 @@
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">
@ -46,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);
}
}

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

@ -1,4 +1,4 @@
part of '../main.dart';
part of 'main.dart';
class EntityState {
static const on = 'on';

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 {

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class ClimateControlWidget extends StatefulWidget {
@ -408,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

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

@ -60,7 +60,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 +100,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",
);

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

@ -47,7 +47,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
.entity;
started = true;
}
streamUrl = '${Connection().httpWebHost}/api/camera_proxy_stream/${_entity
streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
.entityId}?token=${_entity.attributes['access_token']}';
return Column(
children: <Widget>[

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

@ -47,7 +47,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
}
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
_historyLastUpdated = now;
Connection().getHistory(entityId).then((history){
ConnectionManager().getHistory(entityId).then((history){
if (!_disposed) {
setState(() {
_history = history.isNotEmpty ? history[0] : [];

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

@ -23,7 +23,7 @@ class HomeAssistant {
Duration fetchTimeout = Duration(seconds: 30);
String get locationName {
if (Connection().useLovelace) {
if (ConnectionManager().useLovelace) {
return ui?.title ?? "";
} else {
return _instanceConfig["location_name"] ?? "";
@ -36,8 +36,8 @@ class HomeAssistant {
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
HomeAssistant._internal() {
Connection().onStateChangeCallback = _handleEntityStateChange;
Device().loadDeviceInfo();
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
DeviceInfoManager().loadDeviceInfo();
}
Completer _fetchCompleter;
@ -47,18 +47,18 @@ class HomeAssistant {
Logger.w("Previous data fetch is not completed yet");
return _fetchCompleter.future;
}
if (entities == null) entities = EntityCollection(Connection().httpWebHost);
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
_fetchCompleter = Completer();
List<Future> futures = [];
futures.add(_getStates());
if (Connection().useLovelace) {
if (ConnectionManager().useLovelace) {
futures.add(_getLovelace());
}
futures.add(_getConfig());
futures.add(_getServices());
futures.add(_getUserInfo());
futures.add(_getPanels());
futures.add(Connection().sendSocketMessage(
futures.add(ConnectionManager().sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "state_changed"},
));
@ -66,9 +66,9 @@ class HomeAssistant {
if (isMobileAppEnabled) {
_createUI();
_fetchCompleter.complete();
checkAppRegistration();
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")]));
_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);
@ -78,125 +78,15 @@ class HomeAssistant {
Future logout() async {
Logger.d("Logging out...");
await Connection().logout().then((_) {
await ConnectionManager().logout().then((_) {
ui?.clear();
entities?.clear();
panels?.clear();
});
}
Map _getAppRegistrationData() {
return {
"app_version": "$appVersion",
"device_name": "$userName's ${Device().model}",
"manufacturer": Device().manufacturer,
"model": Device().model,
"os_name": Device().osName,
"os_version": Device().osVersion,
"app_data": {
"push_token": "$fcmToken",
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
}
};
}
Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
Completer completer = Completer();
if (Connection().webhookId == null || forceRegister) {
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
var registrationData = _getAppRegistrationData();
registrationData.addAll({
"app_id": "ha_client",
"app_name": "$appName",
"supports_encryption": false,
});
Connection().sendHTTPPost(
endPoint: "/api/mobile_app/registrations",
includeAuthHeader: true,
data: json.encode(registrationData)
).then((response) {
Logger.d("Processing registration responce...");
var responseObject = json.decode(response);
SharedPreferences.getInstance().then((prefs) {
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
Connection().webhookId = responseObject["webhook_id"];
completer.complete();
eventBus.fire(ShowDialogEvent(
title: "Mobile app Integration was created",
body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant",
positiveText: "Restart now",
negativeText: "Later",
onPositive: () {
Connection().callService(domain: "homeassistant", service: "restart", entityId: null);
},
));
});
}).catchError((e) {
completer.complete();
Logger.e("Error registering the app: ${e.toString()}");
});
return completer.future;
} else {
Logger.d("App was previously registered. Checking...");
var updateData = {
"type": "update_registration",
"data": _getAppRegistrationData()
};
Connection().sendHTTPPost(
endPoint: "/api/webhook/${Connection().webhookId}",
includeAuthHeader: false,
data: json.encode(updateData)
).then((response) {
Logger.d("App registration works fine");
if (showOkDialog) {
eventBus.fire(ShowDialogEvent(
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");
eventBus.fire(ShowDialogEvent(
title: "App integration was removed",
body: "Looks like app integration was removed from your Home Assistant. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and other useful stuff.",
positiveText: "Register now",
negativeText: "Cancel",
onPositive: () {
SharedPreferences.getInstance().then((prefs) {
prefs.remove("app-webhook-id");
Connection().webhookId = null;
HomeAssistant().checkAppRegistration();
});
},
));
} else {
Logger.e("Error updating app registration: ${e.toString()}");
eventBus.fire(ShowDialogEvent(
title: "App integration is not working properly",
body: "Something wrong with HA Client integration on your Home Assistant server. Try to remove current app integration from Configuration -> Integrationds using web UI, restart your Home Assistant and go back to the app. NOTE that after clicking 'Ok' current integration data will be removed from the app and new integration wll be created on Home Assistant side on next app launch.",
positiveText: "Ok",
negativeText: "I'll handle it",
onPositive: () {
SharedPreferences.getInstance().then((prefs) {
prefs.remove("app-webhook-id");
Connection().webhookId = null;
HAUtils.launchURL(Connection().httpWebHost+"/config/integrations/dashboard");
});
},
));
}
completer.complete();
});
return completer.future;
}
}
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 HAError("Error getting config: ${e}");
@ -204,7 +94,7 @@ class HomeAssistant {
}
Future _getStates() async {
await Connection().sendSocketMessage(type: "get_states").then(
await ConnectionManager().sendSocketMessage(type: "get_states").then(
(data) => entities.parse(data)
).catchError((e) {
throw HAError("Error getting states: $e");
@ -212,27 +102,27 @@ class HomeAssistant {
}
Future _getLovelace() async {
await Connection().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((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("Services received")).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(
@ -296,31 +186,25 @@ 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']
);
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)));
@ -383,15 +267,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 {
@ -421,7 +305,7 @@ class HomeAssistant {
void _createUI() {
ui = HomeAssistantUI();
if ((Connection().useLovelace) && (_rawLovelaceData != null)) {
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
Logger.d("Creating Lovelace UI");
_parseLovelace();
} else {

View File

@ -1,6 +1,5 @@
import 'dart:convert';
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -15,35 +14,37 @@ 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';
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';
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';
@ -68,33 +69,41 @@ 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 'device.class.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_class/ui.dart';
part 'ui_class/view.class.dart';
part 'ui_class/card.class.dart';
@ -103,16 +112,20 @@ 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 'panels/config_panel_widget.dart';
part 'panels/widgets/link_to_web_config.dart';
part 'utils/logger.dart';
part 'types/ha_error.dart';
part 'types/event_bus_events.dart';
EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = "HA Client";
const appVersion = "0.6.0-alpha3";
const appVersion = "0.6.5";
void main() {
void main() async {
FlutterError.onError = (errorDetails) {
Logger.e( "${errorDetails.exception}");
if (Logger.isInDebugMode) {
@ -121,7 +134,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");
@ -133,7 +150,6 @@ void main() {
class HAClientApp extends StatelessWidget {
final HomeAssistant homeAssistant = HomeAssistant();
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
@ -144,726 +160,39 @@ 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"),
"/putchase": (context) => PurchasePage(title: "Support app development"),
"/log-view": (context) => LogViewPage(title: "Log"),
"/login": (_) => WebviewScaffold(
url: "${Connection().oauthUrl}",
"/login": (context) => WebviewScaffold(
url: "${ConnectionManager().oauthUrl}",
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.help),
onPressed: () => HAUtils.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#authentication")
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#authentication")
),
title: new Text("Login to your Home Assistant"),
),
)
},
);
}
}
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 _showDialogSubscription;
StreamSubscription _reloadUISubscription;
int _previousViewCount;
bool _showLoginButton = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_firebaseMessaging.configure(
onLaunch: (data) {
Logger.d("Notification [onLaunch]: $data");
},
onMessage: (data) {
Logger.d("Notification [onMessage]: $data");
_showNotification(title: data["notification"]["title"], text: data["notification"]["body"]);
},
onResume: (data) {
Logger.d("Notification [onResume]: $data");
}
);
_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) {
_fullLoad();
}
});
_fullLoad();
}
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((_) {
Connection().init(loadSettings: true, forceReconnect: true).then((__){
_fetchData();
}, onError: (e) {
_setErrorState(e);
});
});
}
void _quickLoad() {
_hideBottomBar();
_showInfoBottomBar(progress: true,);
Connection().init(loadSettings: false, forceReconnect: false).then((_){
_fetchData();
}, onError: (e) {
_setErrorState(e);
});
}
_fetchData() async {
await widget.homeAssistant.fetchData().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) {
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 && Connection().settingsLoaded) {
_quickLoad();
}
}
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 (_showDialogSubscription == null) {
_showDialogSubscription = eventBus.on<ShowDialogEvent>().listen((event){
_showDialog(
title: event.title,
body: event.body,
onPositive: event.onPositive,
onNegative: event.onNegative,
positiveText: event.positiveText,
negativeText: event.negativeText
);
});
}
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(event.error);
});
}
if (_startAuthSubscription == null) {
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
setState(() {
_showLoginButton = event.showButton;
});
if (event.showButton) {
_showOAuth();
} else {
Navigator.of(context).pop();
}
});
}
_firebaseMessaging.getToken().then((String token) {
HomeAssistant().fcmToken = token;
completer.complete();
});
return completer.future;
}
void _showOAuth() {
Logger.d("_showOAuth: ${Connection().oauthUrl}");
Navigator.of(context).pushNamed('/login');
}
_setErrorState(HAError e) {
if (e == null) {
_showErrorBottomBar(
HAError("Unknown error")
);
} else {
_showErrorBottomBar(e);
}
}
void _showDialog({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,
);
},
);
}
void _callService(String domain, String service, String entityId, Map additionalParams) {
_showInfoBottomBar(
message: "Calling $domain.$service",
duration: Duration(seconds: 3)
);
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(Connection().displayHostname ?? "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: () {
Navigator.of(context).pop();
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(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(Icons.help),
title: Text("Help"),
onTap: () {
Navigator.of(context).pop();
HAUtils.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();
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
),
),
),
Container(
height: 10.0,
),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
HAUtils.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();
HAUtils.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.URL: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
HAUtils.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 (Connection().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(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") {
_quickLoad();
} else if (val == "logout") {
widget.homeAssistant.logout().then((_) {
_quickLoad();
});
}
});
}
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: emptyBody
),
)
:
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: _buildScaffoldBody(false),
);
}
}
@override
void dispose() {
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.dispose();
WidgetsBinding.instance.removeObserver(this);
_viewsTabController?.dispose();
_stateSubscription?.cancel();
_settingsSubscription?.cancel();
_serviceCallSubscription?.cancel();
_showDialogSubscription?.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 {
@ -14,17 +14,16 @@ class AuthManager {
Completer completer = Completer();
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.onUrlChanged.listen((String url) {
Logger.d("Webview url changed to $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(
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();
@ -38,7 +37,7 @@ class AuthManager {
});
}
});
Logger.d("Launching OAuth: $oauthUrl");
Logger.d("Launching OAuth");
eventBus.fire(StartAuthEvent(oauthUrl, true));
return completer.future;
}

View File

@ -1,14 +1,14 @@
part of 'main.dart';
part of '../main.dart';
class Connection {
class ConnectionManager {
static final Connection _instance = Connection._internal();
static final ConnectionManager _instance = ConnectionManager._internal();
factory Connection() {
factory ConnectionManager() {
return _instance;
}
Connection._internal();
ConnectionManager._internal();
String _domain;
String _port;
@ -54,21 +54,20 @@ class Connection {
completer.completeError(HAError.checkConnectionSettings());
stopInit = true;
} else {
//_token = prefs.getString('hassio-token');
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.");
_token = null;
await storage.delete(key: "hacl_llt");
stopInit = true;
}
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;
}
} else {
if ((_domain == null) || (_port == null) ||
@ -104,13 +103,11 @@ class Connection {
completer?.completeError(HAError("Connection timeout"));
});
}).then((_) {
Logger.d("doConnect is finished 1");
completer?.complete();
}).catchError((e) {
completer?.completeError(e);
});
} else {
Logger.d("doConnect is finished 2");
completer?.complete();
}
}
@ -124,7 +121,7 @@ class Connection {
} else {
connecting = Completer();
_disconnect().then((_) {
Logger.d("Socket connecting: $_webSocketAPIEndpoint...");
Logger.d("Socket connecting...");
_socket = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
_socketSubscription = _socket.stream.listen(
@ -150,9 +147,7 @@ class Connection {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
_messageResolver.remove("auth");
logout().then((_) {
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
});
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
} else {
_handleMessage(data);
}
@ -283,6 +278,7 @@ class Connection {
}
Future logout() {
Logger.d("Logging out");
Completer completer = Completer();
_disconnect().whenComplete(() {
_token = null;
@ -303,13 +299,15 @@ class Connection {
_tempToken = null;
final storage = new FlutterSecureStorage();
storage.write(key: "hacl_llt", value: "$_token").then((_) {
completer.complete();
SharedPreferences.getInstance().then((prefs) {
prefs.setBool("oauth-used", true);
completer.complete();
});
}).catchError((e) {
throw e;
});
}).catchError((e) {
logout();
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.loginAgain()]));
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
});
return completer.future;
}
@ -334,13 +332,13 @@ class Connection {
_connect().timeout(connectTimeout, onTimeout: (){
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
}).then((_) {
Logger.d("[Sending] ==> $rawMessage");
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
_socket.sink.add(rawMessage);
}).catchError((e) {
_completer.completeError(e);
});
} else {
Logger.d("[Sending] ==> $rawMessage");
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
_socket.sink.add(rawMessage);
}
return _completer.future;
@ -369,7 +367,7 @@ class Connection {
//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");
Logger.d("[Sending] ==> HTTP /api/history/period/$startTime?&filter_entity_id=$entityId");
http.Response historyResponse;
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_token",
@ -377,7 +375,7 @@ class Connection {
});
var history = json.decode(historyResponse.body);
if (history is List) {
Logger.d( "[Received] <== ${history.first.length} history recors");
Logger.d( "[Received] <== HTTP ${history.first.length} history recors");
return history;
} else {
return [];
@ -387,7 +385,7 @@ class Connection {
Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async {
Completer completer = Completer();
String url = "$httpWebHost$endPoint";
Logger.d("[Sending] ==> $url");
Logger.d("[Sending] ==> HTTP $endPoint");
Map<String, String> headers = {};
if (contentType != null) {
headers["Content-Type"] = contentType;
@ -400,7 +398,7 @@ class Connection {
headers: headers,
body: data
).then((response) {
Logger.d("[Received] <== ${response.statusCode}, ${response.body}");
Logger.d("[Received] <== HTTP ${response.statusCode}");
if (response.statusCode >= 200 && response.statusCode < 300 ) {
completer.complete(response.body);
} else {

View File

@ -1,10 +1,10 @@
part of 'main.dart';
part of '../main.dart';
class Device {
class DeviceInfoManager {
static final Device _instance = Device._internal();
static final DeviceInfoManager _instance = DeviceInfoManager._internal();
factory Device() {
factory DeviceInfoManager() {
return _instance;
}
@ -14,7 +14,7 @@ class Device {
String osName;
String osVersion;
Device._internal();
DeviceInfoManager._internal();
loadDeviceInfo() {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();

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,46 @@
part of '../main.dart';
class StartupUserMessagesManager {
static final StartupUserMessagesManager _instance = StartupUserMessagesManager
._internal();
factory StartupUserMessagesManager() {
return _instance;
}
StartupUserMessagesManager._internal() {}
bool _supportAppDevelopmentMessageShown;
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload();
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
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);
});
}
));
}
}

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,7 +45,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
// the App.build method, and use it to set our appbar title.
title: new Text(_title),
),
body: 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);

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

@ -0,0 +1,802 @@
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 State<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;
@override
void initState() {
final Stream purchaseUpdates =
InAppPurchaseConnection.instance.purchaseUpdatedStream;
_subscription = purchaseUpdates.listen((purchases) {
_handlePurchaseUpdates(purchases);
});
super.initState();
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();
}
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 {
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: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,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

@ -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 opne web version"),
onTap: () {
Launcher.launchAuthenticatedWebView(context: context, url: this.url, title: this.name);
},
)
],
),
);
}
}

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;
}

View File

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

View File

@ -17,28 +17,26 @@ 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");
Logger.d("New panel '$title'. type=$type, icon=$icon, urlPath=$urlPath");
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: context, url: config["url"]);
} else if (type == "config") {
if (type == "config") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PanelPage(title: "$title", panel: this),
)
);
} else {
String url = "${Connection().httpWebHost}/$urlPath";
Logger.d("Launching custom tab with $url");
HAUtils.launchURLInCustomTab(context: context, url: url);
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}");
}
}

View File

@ -24,6 +24,22 @@ class CardWidget extends StatelessWidget {
}
}
if (card.conditions.isNotEmpty) {
bool showCardByConditions = false;
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 = true;
}
}
if (!showCardByConditions) {
return Container(width: 0.0, height: 0.0,);
}
}
switch (card.type) {
case CardType.entities: {
@ -289,4 +305,4 @@ class CardWidget extends StatelessWidget {
);
}
}
}

View File

@ -1,331 +0,0 @@
part of '../main.dart';
class ConfigPanelWidget extends StatefulWidget {
ConfigPanelWidget({Key key}) : super(key: key);
@override
_ConfigPanelWidgetState createState() => new _ConfigPanelWidgetState();
}
class ConfigurationItem {
ConfigurationItem({ this.isExpanded: false, this.header, this.body });
bool isExpanded;
final String header;
final Widget body;
}
class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
List<ConfigurationItem> _items;
@override
void initState() {
super.initState();
_items = <ConfigurationItem>[
ConfigurationItem(
header: 'Home Assistant Cloud',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/cloud/account");
},
)
],
),
)
),
ConfigurationItem(
header: 'Integrations',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/integrations/dashboard");
},
)
],
),
)
),
ConfigurationItem(
header: 'Users',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/users/picker");
},
)
],
),
)
),
ConfigurationItem(
header: 'General',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/core");
},
),
Container(height: Sizes.rowPadding,),
Text("Server management", style: TextStyle(fontSize: Sizes.largeFontSize)),
Container(height: Sizes.rowPadding,),
Text("Control your Home Assistant server from HA Client."),
Divider(),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Restart', style: TextStyle(color: Colors.blue)),
onPressed: () => restart(),
),
FlatButton(
child: Text("Stop", style: TextStyle(color: Colors.blue)),
onPressed: () => stop(),
),
],
)
],
),
)
),
ConfigurationItem(
header: 'Persons',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/person");
},
)
],
),
)
),
ConfigurationItem(
header: 'Entity Registry',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/entity_registry");
},
)
],
),
)
),
ConfigurationItem(
header: 'Area Registry',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/area_registry");
},
)
],
),
)
),
ConfigurationItem(
header: 'Automation',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/automation");
},
)
],
),
)
),
ConfigurationItem(
header: 'Script',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/script");
},
)
],
),
)
),
ConfigurationItem(
header: 'Customization',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/customize");
},
)
],
),
)
),
ConfigurationItem(
header: 'Mobile app',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text("Registration", style: TextStyle(fontSize: Sizes.largeFontSize)),
Container(height: Sizes.rowPadding,),
Text("${HomeAssistant().userName}'s ${Device().model}, ${Device().osName} ${Device().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.min,
children: <Widget>[
FlatButton(
onPressed: () => updateRegistration(),
child: Text("Check registration", style: TextStyle(color: Colors.blue))
),
FlatButton(
onPressed: () => resetRegistration(),
child: Text("Reset registration", style: TextStyle(color: Colors.red))
)
],
)
],
),
)
)
];
}
restart() {
eventBus.fire(ShowDialogEvent(
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: () {
Connection().callService(domain: "homeassistant", service: "restart", entityId: null);
},
));
}
stop() {
eventBus.fire(ShowDialogEvent(
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: () {
Connection().callService(domain: "homeassistant", service: "stop", entityId: null);
},
));
}
updateRegistration() {
HomeAssistant().checkAppRegistration(showOkDialog: true);
}
resetRegistration() {
eventBus.fire(ShowDialogEvent(
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: () {
HomeAssistant().checkAppRegistration(showOkDialog: true, forceRegister: true);
},
));
}
@override
Widget build(BuildContext context) {
return ListView(
children: [
new ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
_items[index].isExpanded = !_items[index].isExpanded;
});
},
children: _items.map((ConfigurationItem item) {
return new ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return CardHeaderWidget(
name: item.header,
);
},
isExpanded: item.isExpanded,
body: new Container(
child: item.body,
),
);
}).toList(),
),
],
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -1,197 +0,0 @@
part of 'main.dart';
class Logger {
static List<String> _log = [];
static String getLog() {
String res = '';
_log.forEach((line) {
res += "$line\n";
});
return res;
}
static bool get isInDebugMode {
bool inDebugMode = false;
assert(inDebugMode = true);
return inDebugMode;
}
static void e(String message) {
_writeToLog("Error", message);
}
static void w(String message) {
_writeToLog("Warning", message);
}
static void d(String message) {
_writeToLog("Debug", message);
}
static void _writeToLog(String level, String message) {
if (isInDebugMode) {
debugPrint('$message');
}
DateTime t = DateTime.now();
_log.add("${formatDate(t, ["mm","dd"," ","HH",":","nn",":","ss"])} [$level] : $message");
if (_log.length > 100) {
_log.removeAt(0);
}
}
}
class 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.FULL_RELOAD, this.url});
}
class HAErrorActionType {
static const FULL_RELOAD = 0;
static const QUICK_RELOAD = 1;
static const LOGOUT = 2;
static const URL = 3;
static const OPEN_CONNECTION_SETTINGS = 4;
}
class HAUtils {
static void launchURL(String url) async {
if (await urlLauncher.canLaunch(url)) {
await urlLauncher.launch(url);
} else {
Logger.e( "Could not launch $url");
}
}
static void launchURLInCustomTab({BuildContext context, String url, bool enableDefaultShare: true, bool showPageTitle: true}) async {
try {
await launch(
"$url",
option: new CustomTabsOption(
toolbarColor: Theme.of(context).primaryColor,
enableDefaultShare: enableDefaultShare,
enableUrlBarHiding: true,
showPageTitle: showPageTitle,
animation: new CustomTabsAnimation.slideIn()
// or user defined animation.
/*animation: new CustomTabsAnimation(
startEnter: 'slide_up',
startExit: 'android:anim/fade_out',
endEnter: 'android:anim/fade_in',
endExit: 'slide_down',
)*/,
extraCustomTabs: <String>[
// ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox
'org.mozilla.firefox',
// ref. https://play.google.com/store/apps/details?id=com.microsoft.emmx
'com.microsoft.emmx',
],
),
);
} catch (e) {
Logger.w("Can't open custom tab: ${e.toString()}");
Logger.w("Launching in default browser");
HAUtils.launchURL(url);
}
}
}
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 ShowDialogEvent {
final String title;
final String body;
final String positiveText;
final String negativeText;
final onPositive;
final onNegative;
ShowDialogEvent({this.title, this.body, this.positiveText: "Ok", this.negativeText: "Cancel", this.onPositive, this.onNegative});
}
class ShowEntityPageEvent {
Entity entity;
ShowEntityPageEvent(this.entity);
}
class ShowErrorEvent {
final HAError error;
ShowErrorEvent(this.error);
}

69
lib/utils/launcher.dart Normal file
View File

@ -0,0 +1,69 @@
part of '../main.dart';
class Launcher {
static void launchURL(String url) async {
if (await urlLauncher.canLaunch(url)) {
await urlLauncher.launch(url);
} else {
Logger.e( "Could not launch $url");
}
}
static void launchAuthenticatedWebView({BuildContext context, String url, String title}) {
if (url.contains("?")) {
url += "&external_auth=1";
} else {
url += "?external_auth=1";
}
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": "$url",
"title": "${title ?? ''}"
}
);
}
static void launchURLInCustomTab({BuildContext context, String url, bool enableDefaultShare: true, bool showPageTitle: true}) async {
try {
await launch(
"$url",
option: new CustomTabsOption(
toolbarColor: Theme.of(context).primaryColor,
enableDefaultShare: enableDefaultShare,
enableUrlBarHiding: true,
showPageTitle: showPageTitle,
animation: new CustomTabsAnimation.slideIn()
// or user defined animation.
/*animation: new CustomTabsAnimation(
startEnter: 'slide_up',
startExit: 'android:anim/fade_out',
endEnter: 'android:anim/fade_in',
endExit: 'slide_down',
)*/,
extraCustomTabs: <String>[
// ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox
'org.mozilla.firefox',
// ref. https://play.google.com/store/apps/details?id=com.microsoft.emmx
'com.microsoft.emmx',
],
),
);
} catch (e) {
Logger.w("Can't open custom tab: ${e.toString()}");
Logger.w("Launching in default browser");
Launcher.launchURL(url);
}
}
}

46
lib/utils/logger.dart Normal file
View File

@ -0,0 +1,46 @@
part of '../main.dart';
class Logger {
static List<String> _log = [];
static String getLog() {
String res = '';
_log.forEach((line) {
res += "$line\n";
});
return res;
}
static bool get isInDebugMode {
bool inDebugMode = false;
assert(inDebugMode = true);
return inDebugMode;
}
static void e(String message) {
_writeToLog("Error", message);
}
static void w(String message) {
_writeToLog("Warning", message);
}
static void d(String message) {
_writeToLog("Debug", message);
}
static void _writeToLog(String level, String message) {
if (isInDebugMode) {
debugPrint('$message');
}
DateTime t = DateTime.now();
_log.add("${formatDate(t, ["mm","dd"," ","HH",":","nn",":","ss"])} [$level] : $message");
if (_log.length > 100) {
_log.removeAt(0);
}
}
}

View File

@ -49,14 +49,14 @@ packages:
name: charts_common
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.0"
version: "0.8.0"
charts_flutter:
dependency: "direct main"
description:
name: charts_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.0"
version: "0.8.0"
collection:
dependency: transitive
description:
@ -77,7 +77,7 @@ packages:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1+1"
version: "2.1.2"
date_format:
dependency: "direct main"
description:
@ -105,7 +105,7 @@ packages:
name: firebase_messaging
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0+4"
version: "5.1.4"
flutter:
dependency: "direct main"
description: flutter
@ -131,14 +131,14 @@ packages:
name: flutter_launcher_icons
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.2+1"
version: "0.7.3"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.1+3"
version: "0.8.2"
flutter_markdown:
dependency: "direct main"
description:
@ -186,6 +186,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
in_app_purchase:
dependency: "direct main"
description:
name: in_app_purchase
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1+3"
intl:
dependency: transitive
description:
@ -193,6 +200,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.8"
json_annotation:
dependency: transitive
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
logging:
dependency: transitive
description:
@ -295,7 +309,7 @@ packages:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6+3"
version: "1.1.6+4"
stack_trace:
dependency: transitive
description:
@ -379,7 +393,7 @@ packages:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.5"
version: "3.5.0"
yaml:
dependency: transitive
description:

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 0.6.0+603
version: 0.6.5+652
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -18,13 +18,14 @@ dependencies:
date_format: any
charts_flutter: any
flutter_markdown: any
in_app_purchase: ^0.2.1+2
# flutter_svg: ^0.10.3
flutter_custom_tabs: ^0.6.0
firebase_messaging: ^4.0.0+1
flutter_webview_plugin: ^0.3.1
flutter_secure_storage: ^3.2.0
device_info: ^0.4.0+1
flutter_local_notifications: ^0.7.1+3
firebase_messaging: ^5.1.4
flutter_webview_plugin: ^0.3.7
flutter_secure_storage: ^3.2.1+1
device_info: ^0.4.0+2
flutter_local_notifications: ^0.8.2
dev_dependencies:
flutter_test:
@ -47,9 +48,9 @@ flutter:
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
assets:
- images/hassio-192x192.png
- assets/js/externalAuth.js
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.io/assets-and-images/#resolution-aware.