Compare commits

...

84 Commits

Author SHA1 Message Date
9c1d240962 1013 2020-05-06 19:05:11 +00:00
a76652b552 1012 2020-05-06 18:46:05 +00:00
a140f993d0 1011 2020-05-06 18:23:01 +00:00
ded60a2867 not null 2020-05-06 18:22:17 +00:00
b86602bcdb app version and whats new 2020-05-06 18:19:41 +00:00
02ea45469f 1.0.1 2020-05-06 17:52:31 +00:00
90105c3b09 Cache reading issues fix 2020-05-06 17:51:06 +00:00
3d828914cc Fix error handling 2020-05-06 17:24:13 +00:00
8cd5776bc6 Make integration work with HA < 0.104 2020-05-06 16:40:19 +00:00
17ec73b176 Report foreground location errors 2020-05-06 16:19:42 +00:00
e7cce01ca9 Remove beta 2020-05-05 12:34:20 +00:00
6c73f5d979 1008 2020-05-05 12:30:41 +00:00
f59cb5afbf Show error on registration fail 2020-05-05 12:25:34 +00:00
5629215229 Fix app registration error reporting 2020-05-05 12:20:51 +00:00
45fb637d48 Slider fixes. Also resolves #463 2020-05-05 12:12:49 +00:00
7c473eb1ca Avoid null values for slider 2020-05-05 11:35:19 +00:00
b40880c85a Remove some app integration errors 2020-05-05 11:33:04 +00:00
30329ea3ba Update main.dart 2020-05-05 11:41:14 +03:00
ca10401bee 1007 2020-05-04 14:50:23 +00:00
814e0a8b00 Habdle mistakes in card config 2020-05-04 14:47:47 +00:00
b5fbe7b86f Update main.dart 2020-05-04 14:51:55 +03:00
fc9b6f05c0 Update main.dart 2020-05-04 14:51:41 +03:00
eadae4374b Update whats_new.page.dart 2020-05-04 14:49:54 +03:00
711cb04dcf 1.0.0-beta 2020-05-04 12:37:43 +03:00
1d39b7fc7d Update pubspec.yaml 2020-05-04 12:37:01 +03:00
2fa640433a Update whats_new.page.dart 2020-05-04 12:36:32 +03:00
2445dc7869 Update startup_user_messages_manager.class.dart 2020-05-04 12:35:48 +03:00
e3e114fe94 Resolves #551 2020-05-04 12:12:18 +03:00
7a1603b423 alpha2 2020-05-04 12:11:26 +03:00
4b831821da 1005 2020-05-04 12:10:40 +03:00
1ec54953d7 Update .gitignore 2020-05-04 11:32:35 +03:00
61571600d1 Delete google-services.json 2020-05-04 11:31:47 +03:00
07a097aa50 Fix Discord link 2020-05-03 18:56:28 +00:00
ce1cebaf64 Update README.md 2020-05-03 19:48:02 +03:00
faf6f73b2a Update README.md 2020-05-03 19:47:49 +03:00
db3b5d941e 1004 2020-05-03 15:27:22 +00:00
cc60dc2b21 Fix token login popup show 2020-05-03 15:26:44 +00:00
8aa0e03187 1.0.0 alpha1 2020-05-03 14:26:46 +00:00
4492a08d6b battery updated to 1.0.0 2020-05-03 14:12:08 +00:00
792c0d0c84 Resolves #545 handle hidden entities for Lovelace 2020-05-03 14:06:43 +00:00
8221eceb78 Resolves #549 trim spaces for device name, url and long-lived token 2020-05-03 13:47:11 +00:00
12ba5598e4 Resolves #547 Remove close button from quick start 2020-05-03 13:43:04 +00:00
536cbf9445 Resolves #523 Change device name for integration 2020-05-03 13:36:40 +00:00
a87943da27 Resolves #550 socket protocol set 2020-05-03 11:48:09 +00:00
3fddc3b5a7 Resolves #546 Device name is missed 2020-05-03 11:25:09 +00:00
5bc0b0868a 1002 2020-05-03 10:30:51 +00:00
e9ad612fec Fix config panel opening 2020-05-03 10:00:32 +00:00
c62e045dae flutter_secure_storage updated to 3.3.3 2020-05-03 09:58:49 +00:00
725ec9291d WIP #523 and connection settings refactoring 2020-05-02 23:02:18 +00:00
96c8338890 Show error when media_player is not media_player 2020-05-02 08:50:21 +00:00
0996fb94da Fix getting entity on entity page when states is not loaded 2020-05-02 08:31:10 +00:00
5de2431a0f Fix getting default icon of entity without id 2020-05-02 08:27:20 +00:00
163338ea75 WIP #523 Change device name for integration 2020-05-01 22:39:32 +00:00
f28e5493dc Popup menu position fix 2020-05-01 19:15:50 +00:00
01c0a08fa8 Fix media menu closing behaviour 2020-05-01 19:02:42 +00:00
1c461d2449 Fix camera stream view navigation issue 2020-05-01 18:50:50 +00:00
915e8045a3 1001 2020-05-01 17:18:55 +00:00
f10fc7eec1 Resolves #543: Double values in gauge config 2020-05-01 17:09:09 +00:00
320bc677e0 Remove log viewer 2020-05-01 16:33:43 +00:00
46ca1948e2 Error handling improvements 2020-05-01 16:24:13 +00:00
7a0ce93cfd Fix wrong log level 2020-05-01 16:07:22 +00:00
3c0bd68b0a Fix duplicating panels 2020-05-01 16:05:07 +00:00
b4ad3061e4 Fix launchURLInBrowser 2020-05-01 15:49:50 +00:00
d6b1fbec24 Open local navigate actions in browser 2020-05-01 15:48:54 +00:00
cacdd0d304 Fix custom tab opening 2020-05-01 15:40:22 +00:00
e3e1fa3499 Don't throw exception on cached data 2020-05-01 14:42:45 +00:00
58842d1ebb Report all errors to Crashlytivs if in production 2020-05-01 14:37:05 +00:00
101569d6ee Error reporting: HomeAssistant class 2020-05-01 14:37:05 +00:00
8a180c4c0e Panels error handling 2020-05-01 14:37:05 +00:00
ba343fbd98 Final icons update for ald Android versions 2020-04-30 17:14:38 +03:00
1d528df341 Adoptive icon fixes 2020-04-30 14:04:37 +00:00
51ea0b0afa Icons update 2020-04-30 15:52:00 +03:00
9dbb697e58 Adoptive app icon 2020-04-30 12:43:56 +00:00
947558bb3d Icons update 2020-04-30 15:08:26 +03:00
8ba4cc85d8 Bump version to 1.0.0 for fist stable release 2020-04-30 11:23:20 +00:00
0f604a6ce6 New app icon 2020-04-30 13:03:16 +03:00
7e48c6535f 898 2020-04-29 22:39:47 +00:00
1d2a8b613b Disable Crashlytics in debug mode 2020-04-29 22:37:12 +00:00
89e833eb33 Fix theme settings loading 2020-04-29 22:30:53 +00:00
b65a68e0c4 Help pages links 2020-04-29 22:28:52 +00:00
bfb24b9d11 Call service state color from theme 2020-04-29 20:11:37 +00:00
0792cae2b1 Fix missed entity picture 2020-04-29 20:04:59 +00:00
a85fb3d03b Display light color as a badge on entity icon 2020-04-29 20:01:37 +00:00
ddb9a9d4e9 Fix gauge card tap events 2020-04-29 17:48:15 +00:00
72 changed files with 1097 additions and 2186 deletions

1
.gitignore vendored
View File

@ -20,3 +20,4 @@ flutter_export_environment.sh
key.properties key.properties
.secrets.dart .secrets.dart
pubspec.lock pubspec.lock
google-services.json

View File

@ -6,11 +6,13 @@ Visit [ha-client.app](http://ha-client.app/) for more info.
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
Discuss it on [Discord](https://discord.gg/nd6FZQ) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android) Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/estevez-dev/ha_client) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
#### Pre-release CI build #### Stable CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build) [![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5eaef46c513b9f9b25eb7f1a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5eaef46c513b9f9b25eb7f1a/latest_build)
#### Beta CI build #### Beta CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build) [![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
#### Pre-release CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)

View File

@ -1,64 +0,0 @@
{
"project_info": {
"project_number": "441874387819",
"firebase_url": "https://ha-client-c73c4.firebaseio.com",
"project_id": "ha-client-c73c4",
"storage_bucket": "ha-client-c73c4.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:441874387819:android:92c7efc892dc3d45",
"android_client_info": {
"package_name": "com.keyboardcrumbs.haclient"
}
},
"oauth_client": [
{
"client_id": "441874387819-uqmkibhf361828od1982o2jhl0n3m0ov.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.keyboardcrumbs.haclient",
"certificate_hash": "bebe4d970fbebf0bff2c93244fdc7fcbcefb3470"
}
},
{
"client_id": "441874387819-5q7vmimci4s2jl3v0ncugv1ocp4m48nb.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.keyboardcrumbs.haclient",
"certificate_hash": "0ea12348468be44bc2aa5792ee7e8924c633da81"
}
},
{
"client_id": "441874387819-joi8plo5345ebt8i1dug27u2aenv5tg7.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.keyboardcrumbs.haclient",
"certificate_hash": "fcbc805d965ccf6a4d5417398d191edc9c9890b0"
}
},
{
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBsl9cjBY633IrdrTyCsLFlO9lfsYJ0OJU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@ -8,7 +8,6 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- io.flutter.app.FlutterApplication is an android.app.Application that <!-- io.flutter.app.FlutterApplication is an android.app.Application that
@ -20,6 +19,7 @@
android:name="io.flutter.app.FlutterApplication" android:name="io.flutter.app.FlutterApplication"
android:label="HA Client" android:label="HA Client"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
<meta-data <meta-data

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group>
<clip-path
android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" />
<path
android:fillColor="#709ac1"
android:fillAlpha="0"
android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" />
<path
android:fillColor="#000000"
android:fillAlpha="0.12"
android:pathData="M 70.506 38.389 L 108 72.466 L 108 108 L 77 108 L 35.066 72.466 L 38.373 63.769 L 36.268 50.216 L 43.335 44.523 L 51.841 34.578 L 63.096 42.478 L 68.586 42.478 L 70.506 38.389 Z" />
<path
android:fillColor="#000000"
android:fillAlpha="0.12"
android:pathData="M 28.979 53.708 L 47.736 67.31 L 38.373 58.563 L 36.268 51.52 L 28.979 53.708 Z" />
<path
android:fillColor="#ffffff"
android:pathData="M 77.131 54.24 L 72.878 54.24 L 72.878 72.415 L 56.339 72.415 L 56.339 64.85 L 62.931 58.511 L 64.609 58.784 C 67.349 58.784 69.57 56.649 69.57 54.013 C 69.57 51.378 67.349 49.242 64.609 49.242 C 61.868 49.242 59.647 51.378 59.647 54.013 L 59.883 55.626 L 56.339 59.079 L 56.339 46.63 C 57.898 45.812 58.938 44.244 58.938 42.427 C 58.938 39.792 56.717 37.656 53.976 37.656 C 51.236 37.656 49.015 39.792 49.015 42.427 C 49.015 44.244 50.054 45.812 51.614 46.63 L 51.614 59.079 L 48.07 55.626 L 48.306 54.013 C 48.306 51.378 46.084 49.242 43.344 49.242 C 40.604 49.242 38.383 51.378 38.383 54.013 C 38.383 56.648 40.604 58.784 43.344 58.784 L 45.022 58.511 L 51.614 64.85 L 51.614 72.415 L 35.075 72.415 L 35.075 54.24 L 30.94 54.24 C 29.948 54.24 28.979 54.24 28.979 53.763 C 29.003 53.263 29.995 52.309 31.011 51.332 L 51.614 31.522 C 52.393 30.772 53.197 30 53.976 30 C 54.756 30 55.559 30.772 56.339 31.522 L 65.79 40.609 L 65.79 38.338 L 70.515 38.338 L 70.515 45.153 L 77.084 51.469 C 78.029 52.377 78.997 53.309 79.021 53.786 C 79.021 54.24 78.076 54.24 77.131 54.24 Z M 43.344 51.969 C 43.908 51.969 44.449 52.184 44.848 52.567 C 45.247 52.951 45.471 53.471 45.471 54.013 C 45.471 54.555 45.247 55.076 44.848 55.459 C 44.449 55.842 43.908 56.058 43.344 56.058 C 42.78 56.058 42.239 55.842 41.841 55.459 C 41.442 55.076 41.218 54.555 41.218 54.013 C 41.218 53.471 41.442 52.951 41.841 52.567 C 42.239 52.184 42.78 51.969 43.344 51.969 Z M 64.609 51.969 C 65.79 51.969 66.735 52.877 66.735 54.013 C 66.735 55.149 65.79 56.058 64.609 56.058 C 64.045 56.058 63.504 55.842 63.105 55.459 C 62.706 55.076 62.482 54.555 62.482 54.013 C 62.482 53.471 62.706 52.951 63.105 52.567 C 63.504 52.184 64.045 51.969 64.609 51.969 Z M 53.976 40.382 C 55.158 40.382 56.103 41.291 56.103 42.427 C 56.103 43.563 55.158 44.472 53.976 44.472 C 52.795 44.472 51.85 43.563 51.85 42.427 C 51.85 41.291 52.795 40.382 53.976 40.382 Z" />
</group>
</vector>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen --> <!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" /> <item android:drawable="@color/main_color" />
<!-- You can insert your own image assets here --> <!-- You can insert your own image assets here -->
<!-- <item> <!-- <item>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/main_color"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/main_color"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="main_color">#709AC1</color>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when <!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame --> Flutter draws its first frame -->

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@ -33,7 +33,7 @@ class AlarmPanelCard extends StatelessWidget {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.getIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")), "mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.entity.entity)) onPressed: () => eventBus.fire(new ShowEntityPageEvent(entityId: card.entity.entity.entityId))
) )
) )
] ]

View File

@ -11,68 +11,68 @@ class CardData {
EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null; EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null;
factory CardData.parse(Map<String, dynamic> rawData) { factory CardData.parse(rawData) {
try { try {
switch (rawData['type']) { switch (rawData['type']) {
case CardType.ENTITIES: case CardType.ENTITIES:
return EntitiesCardData(rawData);
break;
case CardType.ALARM_PANEL:
return AlarmPanelCardData(rawData);
break;
case CardType.BUTTON:
return ButtonCardData(rawData);
break;
case CardType.ENTITY_BUTTON:
return ButtonCardData(rawData);
break;
case CardType.CONDITIONAL:
return CardData.parse(rawData['card']);
break;
case CardType.ENTITY_FILTER:
Map<String, dynamic> cardData = Map.from(rawData);
cardData.remove('type');
if (rawData.containsKey('card')) {
cardData.addAll(rawData['card']);
}
cardData['type'] ??= CardType.ENTITIES;
return CardData.parse(cardData);
break;
case CardType.GAUGE:
return GaugeCardData(rawData);
break;
case CardType.GLANCE:
return GlanceCardData(rawData);
break;
case CardType.HORIZONTAL_STACK:
return HorizontalStackCardData(rawData);
break;
case CardType.VERTICAL_STACK:
return VerticalStackCardData(rawData);
break;
case CardType.MARKDOWN:
return MarkdownCardData(rawData);
break;
case CardType.MEDIA_CONTROL:
return MediaControlCardData(rawData);
break;
default:
if (rawData.containsKey('entities')) {
return EntitiesCardData(rawData); return EntitiesCardData(rawData);
} else if (rawData.containsKey('entity')) { break;
rawData['entities'] = [rawData['entity']]; case CardType.ALARM_PANEL:
return EntitiesCardData(rawData); return AlarmPanelCardData(rawData);
} break;
return CardData(rawData); case CardType.BUTTON:
} return ButtonCardData(rawData);
} catch (error) { break;
Logger.e('Error parsing card: $error'); case CardType.ENTITY_BUTTON:
return ButtonCardData(rawData);
break;
case CardType.CONDITIONAL:
return CardData.parse(rawData['card']);
break;
case CardType.ENTITY_FILTER:
Map cardData = Map.from(rawData);
cardData.remove('type');
if (rawData.containsKey('card')) {
cardData.addAll(rawData['card']);
}
cardData['type'] ??= CardType.ENTITIES;
return CardData.parse(cardData);
break;
case CardType.GAUGE:
return GaugeCardData(rawData);
break;
case CardType.GLANCE:
return GlanceCardData(rawData);
break;
case CardType.HORIZONTAL_STACK:
return HorizontalStackCardData(rawData);
break;
case CardType.VERTICAL_STACK:
return VerticalStackCardData(rawData);
break;
case CardType.MARKDOWN:
return MarkdownCardData(rawData);
break;
case CardType.MEDIA_CONTROL:
return MediaControlCardData(rawData);
break;
default:
if (rawData.containsKey('entities')) {
return EntitiesCardData(rawData);
} else if (rawData.containsKey('entity')) {
rawData['entities'] = [rawData['entity']];
return EntitiesCardData(rawData);
}
return CardData(rawData);
}
} catch (error, stacktrace) {
Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace);
return ErrorCardData(rawData); return ErrorCardData(rawData);
} }
} }
CardData(Map<String, dynamic> rawData) { CardData(rawData) {
if (rawData != null) { if (rawData != null && rawData is Map) {
type = rawData['type'] ?? CardType.ENTITIES; type = rawData['type'] ?? CardType.ENTITIES;
conditions = rawData['conditions'] ?? []; conditions = rawData['conditions'] ?? [];
showEmpty = rawData['show_empty'] ?? true; showEmpty = rawData['show_empty'] ?? true;
@ -91,7 +91,7 @@ class CardData {
List<EntityWrapper> getEntitiesToShow() { List<EntityWrapper> getEntitiesToShow() {
return entities.where((entityWrapper) { return entities.where((entityWrapper) {
if (HomeAssistant().autoUi && entityWrapper.entity.isHidden) { if (entityWrapper.entity.isHidden) {
return false; return false;
} }
List currentStateFilter; List currentStateFilter;
@ -148,9 +148,8 @@ class CardData {
break; break;
} }
} }
} catch (e) { } catch (e, stacktrace) {
Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState'); Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState: $e', stacktrace: stacktrace);
Logger.e('$e');
} }
} }
} }
@ -171,7 +170,7 @@ class EntitiesCardData extends CardData {
return EntitiesCard(card: this); return EntitiesCard(card: this);
} }
EntitiesCardData(Map<String, dynamic> rawData) : super(rawData) { EntitiesCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
title = rawData["title"]; title = rawData["title"];
icon = rawData['icon']; icon = rawData['icon'];
@ -261,7 +260,7 @@ class AlarmPanelCardData extends CardData {
return AlarmPanelCard(card: this); return AlarmPanelCard(card: this);
} }
AlarmPanelCardData(Map<String, dynamic> rawData) : super(rawData) { AlarmPanelCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
name = rawData['name']; name = rawData['name'];
states = rawData['states']; states = rawData['states'];
@ -297,7 +296,7 @@ class ButtonCardData extends CardData {
return EntityButtonCard(card: this); return EntityButtonCard(card: this);
} }
ButtonCardData(Map<String, dynamic> rawData) : super(rawData) { ButtonCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
name = rawData['name']; name = rawData['name'];
icon = rawData['icon']; icon = rawData['icon'];
@ -351,8 +350,8 @@ class GaugeCardData extends CardData {
String name; String name;
String unit; String unit;
int min; double min;
int max; double max;
Map severity; Map severity;
@override @override
@ -360,12 +359,24 @@ class GaugeCardData extends CardData {
return GaugeCard(card: this); return GaugeCard(card: this);
} }
GaugeCardData(Map<String, dynamic> rawData) : super(rawData) { GaugeCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
name = rawData['name']; name = rawData['name'];
unit = rawData['unit']; unit = rawData['unit'];
min = rawData['min'] ?? 0; if (rawData['min'] is int) {
max = rawData['max'] ?? 100; min = rawData['min'].toDouble();
} else if (rawData['min'] is double) {
min = rawData['min'];
} else {
min = 0;
}
if (rawData['max'] is int) {
max = rawData['max'].toDouble();
} else if (rawData['max'] is double) {
max = rawData['max'];
} else {
max = 100;
}
severity = rawData['severity']; severity = rawData['severity'];
//Parsing entity //Parsing entity
var entitiId = rawData["entity"]; var entitiId = rawData["entity"];
@ -400,7 +411,7 @@ class GlanceCardData extends CardData {
return GlanceCard(card: this); return GlanceCard(card: this);
} }
GlanceCardData(Map<String, dynamic> rawData) : super(rawData) { GlanceCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
title = rawData["title"]; title = rawData["title"];
showName = rawData['show_name'] ?? true; showName = rawData['show_name'] ?? true;
@ -448,7 +459,7 @@ class HorizontalStackCardData extends CardData {
return HorizontalStackCard(card: this); return HorizontalStackCard(card: this);
} }
HorizontalStackCardData(Map<String, dynamic> rawData) : super(rawData) { HorizontalStackCardData(rawData) : super(rawData) {
if (rawData.containsKey('cards')) { if (rawData.containsKey('cards')) {
childCards = rawData['cards'].map<CardData>((childCard) { childCards = rawData['cards'].map<CardData>((childCard) {
return CardData.parse(childCard); return CardData.parse(childCard);
@ -469,7 +480,7 @@ class VerticalStackCardData extends CardData {
return VerticalStackCard(card: this); return VerticalStackCard(card: this);
} }
VerticalStackCardData(Map<String, dynamic> rawData) : super(rawData) { VerticalStackCardData(rawData) : super(rawData) {
if (rawData.containsKey('cards')) { if (rawData.containsKey('cards')) {
childCards = rawData['cards'].map<CardData>((childCard) { childCards = rawData['cards'].map<CardData>((childCard) {
return CardData.parse(childCard); return CardData.parse(childCard);
@ -491,7 +502,7 @@ class MarkdownCardData extends CardData {
return MarkdownCard(card: this); return MarkdownCard(card: this);
} }
MarkdownCardData(Map<String, dynamic> rawData) : super(rawData) { MarkdownCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
title = rawData['title']; title = rawData['title'];
content = rawData['content']; content = rawData['content'];
@ -506,7 +517,7 @@ class MediaControlCardData extends CardData {
return MediaControlsCard(card: this); return MediaControlsCard(card: this);
} }
MediaControlCardData(Map<String, dynamic> rawData) : super(rawData) { MediaControlCardData(rawData) : super(rawData) {
var entitiId = rawData["entity"]; var entitiId = rawData["entity"];
if (entitiId != null && entitiId is String) { if (entitiId != null && entitiId is String) {
if (HomeAssistant().entities.isExist(entitiId)) { if (HomeAssistant().entities.isExist(entitiId)) {
@ -530,7 +541,7 @@ class ErrorCardData extends CardData {
return ErrorCard(card: this); return ErrorCard(card: this);
} }
ErrorCardData(Map<String, dynamic> rawData) : super(rawData) { ErrorCardData(rawData) : super(rawData) {
cardConfig = '$rawData'; cardConfig = '$rawData';
} }

View File

@ -26,7 +26,7 @@ class ErrorCard extends StatelessWidget {
), ),
RaisedButton( RaisedButton(
onPressed: () { onPressed: () {
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title="); Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
}, },
child: Text('Report issue'), child: Text('Report issue'),
) )

View File

@ -110,35 +110,38 @@ class GaugeCard extends StatelessWidget {
child: Stack( child: Stack(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: <Widget>[ children: <Widget>[
SfRadialGauge( IgnorePointer(
axes: <RadialAxis>[ ignoring: true,
RadialAxis( child: SfRadialGauge(
maximum: card.max.toDouble(), axes: <RadialAxis>[
minimum: card.min.toDouble(), RadialAxis(
showLabels: false, maximum: card.max.toDouble(),
useRangeColorForAxis: true, minimum: card.min.toDouble(),
showTicks: false, showLabels: false,
canScaleToFit: true, useRangeColorForAxis: true,
ranges: ranges, showTicks: false,
axisLineStyle: AxisLineStyle( canScaleToFit: true,
thickness: 0.3, ranges: ranges,
thicknessUnit: GaugeSizeUnit.factor, axisLineStyle: AxisLineStyle(
color: Colors.transparent thickness: 0.3,
), thicknessUnit: GaugeSizeUnit.factor,
startAngle: 180, color: Colors.transparent
endAngle: 0, ),
pointers: <GaugePointer>[ startAngle: 180,
RangePointer( endAngle: 0,
value: fixedValue, pointers: <GaugePointer>[
sizeUnit: GaugeSizeUnit.factor, RangePointer(
width: 0.3, value: fixedValue,
color: currentColor, sizeUnit: GaugeSizeUnit.factor,
enableAnimation: true, width: 0.3,
animationType: AnimationType.bounceOut, color: currentColor,
) enableAnimation: true,
] animationType: AnimationType.bounceOut,
) )
], ]
)
],
)
), ),
Column( Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,

View File

@ -13,6 +13,14 @@ class MediaControlsCard extends StatelessWidget {
child: MissedEntityWidget(), child: MissedEntityWidget(),
handleTap: false, handleTap: false,
); );
} else if (card.entity.entity.domain == null || card.entity.entity.domain != 'media_player') {
return EntityModel(
entityWrapper: card.entity,
child: ErrorEntityWidget(
text: '${card.entity.entity?.entityId} is not a media_player',
),
handleTap: false,
);
} }
return CardWrapper( return CardWrapper(
child: EntityModel( child: EntityModel(

View File

@ -143,6 +143,6 @@ class BadgeWidget extends StatelessWidget {
], ],
), ),
onTap: () => onTap: () =>
eventBus.fire(new ShowEntityPageEvent(entity: entityModel.entityWrapper.entity))); eventBus.fire(new ShowEntityPageEvent(entityId: entityModel.entityWrapper.entity.entityId)));
} }
} }

View File

@ -138,8 +138,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
iconSize: 40, iconSize: 40,
color: Theme.of(context).accentColor, color: Theme.of(context).accentColor,
onPressed: _isLoaded ? () { onPressed: _isLoaded ? () {
eventBus.fire(ShowEntityPageEvent()); Navigator.of(context).pushReplacement(
Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (conext) => FullScreenPage( builder: (conext) => FullScreenPage(
child: EntityModel( child: EntityModel(
@ -154,8 +153,8 @@ class _CameraStreamViewState extends State<CameraStreamView> {
), ),
fullscreenDialog: true fullscreenDialog: true
) )
).then((_) { ).then((_){
eventBus.fire(ShowEntityPageEvent(entity: _entity)); eventBus.fire(ShowEntityPageEvent(entityId: _entity.entityId));
}); });
} : null, } : null,
) )

View File

@ -191,7 +191,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final entityModel = EntityModel.of(context); final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entityWrapper.entity; final ClimateEntity entity = entityModel.entityWrapper.entity;
Logger.d("[Climate widget build] changed here = $_changedHere");
if (_changedHere) { if (_changedHere) {
//_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow); //_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
_changedHere = false; _changedHere = false;
@ -353,15 +352,19 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
} }
Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) { Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) {
List<Widget> result = [];
if (entity.supportTargetHumidity) { if (entity.supportTargetHumidity) {
result.addAll(<Widget>[ return Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
"$_tmpTargetHumidity%", children: <Widget>[
style: Theme.of(context).textTheme.display1, Padding(
), padding: EdgeInsets.only(top: Sizes.rowPadding),
Expanded( child: Text("Target humidity", style: Theme.of(context).textTheme.body1),
child: Slider( ),
UniversalSlider(
leading: Text(
"$_tmpTargetHumidity%",
style: Theme.of(context).textTheme.display1,
),
value: _tmpTargetHumidity, value: _tmpTargetHumidity,
max: entity.maxHumidity, max: entity.maxHumidity,
min: entity.minHumidity, min: entity.minHumidity,
@ -373,22 +376,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}), }),
onChangeEnd: (double v) => _setTargetHumidity(entity, v), onChangeEnd: (double v) => _setTargetHumidity(entity, v),
), ),
)
]);
}
if (result.isNotEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Target humidity", style: Theme.of(context).textTheme.body1),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: result,
),
Container( Container(
height: Sizes.rowPadding, height: Sizes.rowPadding,
) )

View File

@ -62,11 +62,10 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.only(top: Sizes.rowPadding),
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Position"), child: Text("Position"),
), ),
Slider( UniversalSlider(
value: _tmpPosition, value: _tmpPosition,
min: 0.0, min: 0.0,
max: 100.0, max: 100.0,
@ -78,8 +77,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
}); });
}, },
onChangeEnd: (double value) => _setNewPosition(entity, value), onChangeEnd: (double value) => _setNewPosition(entity, value),
), )
Container(height: Sizes.rowPadding,)
], ],
); );
} else { } else {
@ -96,7 +94,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
} }
if (entity.supportSetTiltPosition) { if (entity.supportSetTiltPosition) {
controls.addAll(<Widget>[ controls.addAll(<Widget>[
Slider( UniversalSlider(
value: _tmpTiltPosition, value: _tmpTiltPosition,
min: 0.0, min: 0.0,
max: 100.0, max: 100.0,
@ -115,7 +113,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
if (controls.isNotEmpty) { if (controls.isNotEmpty) {
controls.insert(0, Padding( controls.insert(0, Padding(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), 0.0, Sizes.rowPadding, 0.0, 0),
child: Text("Tilt position"), child: Text("Tilt position"),
)); ));
return Column( return Column(

View File

@ -36,7 +36,6 @@ class DefaultEntityContainer extends StatelessWidget {
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: <Widget>[ children: <Widget>[
EntityIcon(), EntityIcon(),
Flexible( Flexible(
fit: FlexFit.tight, fit: FlexFit.tight,
flex: 3, flex: 3,

View File

@ -148,7 +148,7 @@ class Entity {
void update(Map rawData, String webHost) { void update(Map rawData, String webHost) {
attributes = rawData["attributes"] ?? {}; attributes = rawData["attributes"] ?? {};
domain = rawData["entity_id"].split(".")[0]; domain = rawData["entity_id"] != null ? rawData["entity_id"].split(".")[0] : null;
entityId = rawData["entity_id"]; entityId = rawData["entity_id"];
deviceClass = attributes["device_class"]; deviceClass = attributes["device_class"];
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"]; state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"];

View File

@ -9,6 +9,9 @@ class EntityIcon extends StatelessWidget {
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key); const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key);
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) { int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
if (entityId == null) {
return 0;
}
String domain = entityId.split(".")[0]; String domain = entityId.split(".")[0];
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"]; String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
String iconNameByDeviceClass; String iconNameByDeviceClass;
@ -23,12 +26,13 @@ class EntityIcon extends StatelessWidget {
} }
} }
Widget buildIcon(EntityWrapper data, Color color) { Widget buildIcon(BuildContext context, EntityWrapper data, Color color) {
Widget result;
if (data == null) { if (data == null) {
return null; return null;
} }
if (data.entityPicture != null) { if (data.entityPicture != null) {
return Container( result = Container(
height: size+12, height: size+12,
width: size+12, width: size+12,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -41,20 +45,55 @@ class EntityIcon extends StatelessWidget {
) )
), ),
); );
}
String iconName = data.icon;
int iconCode = 0;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else { } else {
iconCode = getDefaultIconByEntityId(data.entity.entityId, String iconName = data.icon;
data.entity.deviceClass, data.entity.state); // int iconCode = 0;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); //
}
result = Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
);
if (data.entity is LightEntity &&
(data.entity as LightEntity).supportColor &&
(data.entity as LightEntity).color != null
) {
Color lightColor = (data.entity as LightEntity).color.toColor();
if (lightColor == Colors.white) {
return result;
}
result = Stack(
children: <Widget>[
result,
Positioned(
bottom: 0,
right: 0,
child: Container(
width: size / 3,
height: size / 3,
decoration: BoxDecoration(
color: lightColor,
shape: BoxShape.circle,
boxShadow: <BoxShadow>[
BoxShadow(
spreadRadius: 0,
blurRadius: 0,
offset: Offset(0.3, 0.3)
)
]
),
),
)
],
);
}
} }
return Icon( return result;
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
);
} }
@override @override
@ -71,6 +110,7 @@ class EntityIcon extends StatelessWidget {
return Padding( return Padding(
padding: padding, padding: padding,
child: buildIcon( child: buildIcon(
context,
entityWrapper, entityWrapper,
iconColor iconColor
), ),

View File

@ -2,10 +2,9 @@ part of '../main.dart';
class EntityPageLayout extends StatelessWidget { class EntityPageLayout extends StatelessWidget {
final bool showClose;
final Entity entity; final Entity entity;
EntityPageLayout({Key key, this.showClose: false, this.entity}) : super(key: key); EntityPageLayout({Key key, this.entity}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -14,34 +13,6 @@ class EntityPageLayout extends StatelessWidget {
child: ListView( child: ListView(
padding: EdgeInsets.all(0), padding: EdgeInsets.all(0),
children: <Widget>[ children: <Widget>[
showClose ?
Container(
color: Theme.of(context).primaryColor,
height: 40,
child: Row(
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8),
child: Text(
entity.displayName,
style: Theme.of(context).primaryTextTheme.headline
),
),
),
IconButton(
padding: EdgeInsets.all(0),
icon: Icon(Icons.close),
color: Theme.of(context).primaryTextTheme.headline.color,
iconSize: 36.0,
onPressed: () {
eventBus.fire(ShowEntityPageEvent());
},
)
],
),
) :
Container(height: 0, width: 0,),
Padding( Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding), padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)), child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)),

View File

@ -54,16 +54,16 @@ class EntityWrapper {
case EntityUIAction.moreInfo: { case EntityUIAction.moreInfo: {
eventBus.fire( eventBus.fire(
new ShowEntityPageEvent(entity: entity)); new ShowEntityPageEvent(entityId: entity.entityId));
break; break;
} }
case EntityUIAction.navigate: { case EntityUIAction.navigate: {
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) { if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
//TODO handle local urls //TODO handle local urls
Logger.w("Local urls is not supported yet"); Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.tapService}');
} else { } else {
Launcher.launchURL(uiAction.tapService); Launcher.launchURLInBrowser(uiAction.tapService);
} }
break; break;
} }
@ -94,16 +94,16 @@ class EntityWrapper {
case EntityUIAction.moreInfo: { case EntityUIAction.moreInfo: {
eventBus.fire( eventBus.fire(
new ShowEntityPageEvent(entity: entity)); new ShowEntityPageEvent(entityId: entity.entityId));
break; break;
} }
case EntityUIAction.navigate: { case EntityUIAction.navigate: {
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) { if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
//TODO handle local urls //TODO handle local urls
Logger.w("Local urls is not supported yet"); Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.holdService}');
} else { } else {
Launcher.launchURL(uiAction.holdService); Launcher.launchURLInBrowser(uiAction.holdService);
} }
break; break;
} }
@ -134,16 +134,16 @@ class EntityWrapper {
case EntityUIAction.moreInfo: { case EntityUIAction.moreInfo: {
eventBus.fire( eventBus.fire(
new ShowEntityPageEvent(entity: entity)); new ShowEntityPageEvent(entityId: entity.entityId));
break; break;
} }
case EntityUIAction.navigate: { case EntityUIAction.navigate: {
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) { if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
//TODO handle local urls //TODO handle local urls
Logger.w("Local urls is not supported yet"); Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.doubleTapService}');
} else { } else {
Launcher.launchURL(uiAction.doubleTapService); Launcher.launchURLInBrowser(uiAction.doubleTapService);
} }
break; break;
} }

View File

@ -0,0 +1,23 @@
part of '../main.dart';
class ErrorEntityWidget extends StatelessWidget {
final String text;
ErrorEntityWidget({
Key key, this.text
}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
String errorText = text ?? "Entity error: ${entityModel.entityWrapper.entity?.entityId}";
return Container(
child: Padding(
padding: EdgeInsets.all(5.0),
child: Text(errorText),
),
color: Theme.of(context).errorColor,
);
}
}

View File

@ -83,8 +83,7 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}") Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
], ],
), ),
Container(height: 10,), UniversalSlider(
Slider(
min: 0, min: 0,
activeColor: Theme.of(context).accentColor, activeColor: Theme.of(context).accentColor,
max: entity.durationSeconds.toDouble(), max: entity.durationSeconds.toDouble(),

View File

@ -228,7 +228,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")), "mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: entity)) onPressed: () => eventBus.fire(new ShowEntityPageEvent(entityId: entity.entityId))
) )
); );
} else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) { } else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) {

View File

@ -27,7 +27,7 @@ class SimpleEntityState extends StatelessWidget {
tStyle = textStyle; tStyle = textStyle;
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.callService) { } else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.callService) {
tStyle = Theme.of(context).textTheme.subhead.copyWith( tStyle = Theme.of(context).textTheme.subhead.copyWith(
color: Colors.blue color: HAClientTheme().getLinkTextStyle(context).color
); );
} else { } else {
tStyle = Theme.of(context).textTheme.body1; tStyle = Theme.of(context).textTheme.body1;

View File

@ -40,7 +40,7 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
} else { } else {
_changedHere = false; _changedHere = false;
} }
Widget slider = Slider( Widget slider = UniversalSlider(
min: entity.minValue * _multiplier, min: entity.minValue * _multiplier,
max: entity.maxValue * _multiplier, max: entity.maxValue * _multiplier,
value: (_newValue <= entity.maxValue) && value: (_newValue <= entity.maxValue) &&

View File

@ -29,8 +29,8 @@ class TimerEntity extends Entity {
Logger.e("Strange $entityId duration format: $durationSource"); Logger.e("Strange $entityId duration format: $durationSource");
duration = Duration(seconds: 0); duration = Duration(seconds: 0);
} }
} catch (e) { } catch (e, stacktrace) {
Logger.e("Error parsing duration for $entityId: ${e.toString()}"); Logger.e("Error parsing duration for $entityId: $e", stacktrace: stacktrace);
duration = Duration(seconds: 0); duration = Duration(seconds: 0);
} }
} else { } else {

View File

@ -30,8 +30,8 @@ class _TimerStateState extends State<TimerState> {
.difference(entity.lastUpdatedTimestamp) .difference(entity.lastUpdatedTimestamp)
.inSeconds; .inSeconds;
remaining = Duration(seconds: entity.duration.inSeconds - passed); remaining = Duration(seconds: entity.duration.inSeconds - passed);
} catch (e) { } catch (e, stacktrace) {
Logger.e("Error calculating ${entity.entityId} remaining time: ${e.toString()}"); Logger.e("Error calculating ${entity.entityId} remaining time: $e", stacktrace: stacktrace);
remaining = Duration(seconds: 0); remaining = Duration(seconds: 0);
} }
}); });

View File

@ -2,17 +2,20 @@ part of '../main.dart';
class UniversalSlider extends StatefulWidget { class UniversalSlider extends StatefulWidget {
final onChanged; final Function onChanged;
final onChangeEnd; final Function onChangeEnd;
final Function onChangeStart;
final Widget leading; final Widget leading;
final Widget closing; final Widget closing;
final String title; final String title;
final double min; final double min;
final Color activeColor;
final double max; final double max;
final double value; final double value;
final int divisions;
final EdgeInsets padding; final EdgeInsets padding;
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key); const UniversalSlider({Key key, this.onChanged, this.onChangeStart, this.activeColor, this.divisions, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
@override @override
State<StatefulWidget> createState() { State<StatefulWidget> createState() {
@ -35,32 +38,36 @@ class UniversalSliderState extends State<UniversalSlider> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List <Widget> row = []; List <Widget> row = [];
List <Widget> col = [];
if (widget.leading != null) { if (widget.leading != null) {
row.add(widget.leading); row.add(widget.leading);
} }
row.add( row.add(
Flexible( Flexible(
child: Slider( child: Slider(
value: _value, value: _value ?? math.max(widget.max ?? 100, _value ?? 0),
min: widget.min, min: widget.min ?? 0,
max: widget.max, max: widget.max ?? 100,
onChangeStart: (_) { activeColor: widget.activeColor,
onChangeStart: (value) {
_changeStarted = true; _changeStarted = true;
widget.onChangeStart?.call(value);
}, },
divisions: widget.divisions,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_value = value; _value = value;
}); });
widget.onChanged(value); widget.onChanged?.call(value);
}, },
onChangeEnd: (value) { onChangeEnd: (value) {
_changeStarted = false; _changeStarted = false;
setState(() {
_value = value;
});
Timer(Duration(milliseconds: 500), () { Timer(Duration(milliseconds: 500), () {
if (!_changeStarted) { if (!_changeStarted) {
setState(() { widget.onChangeEnd?.call(value);
_value = value;
});
widget.onChangeEnd(value);
} }
}); });
} }
@ -70,20 +77,25 @@ class UniversalSliderState extends State<UniversalSlider> {
if (widget.closing != null) { if (widget.closing != null) {
row.add(widget.closing); row.add(widget.closing);
} }
if (widget.title != null) {
col.addAll(<Widget>[
Container(height: Sizes.rowPadding,),
Text('${widget.title}'),
]);
}
col.addAll(<Widget>[
Container(height: Sizes.rowPadding,),
Row(
mainAxisSize: MainAxisSize.min,
children: row,
),
Container(height: Sizes.rowPadding,)
]);
return Padding( return Padding(
padding: widget.padding, padding: widget.padding,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: col,
Container(height: Sizes.rowPadding,),
Text('${widget.title}'),
Container(height: Sizes.rowPadding,),
Row(
mainAxisSize: MainAxisSize.min,
children: row,
),
Container(height: Sizes.rowPadding,)
],
), ),
); );
} }

View File

@ -42,7 +42,7 @@ class HomeAssistant {
return _instanceConfig["location_name"] ?? "Home"; return _instanceConfig["location_name"] ?? "Home";
} }
} }
String get userName => _userName ?? locationName; String get userName => _userName ?? '';
String get userAvatarText => userName.length > 0 ? userName[0] : ""; String get userAvatarText => userName.length > 0 ? userName[0] : "";
bool get isNoEntities => entities == null || entities.isEmpty; bool get isNoEntities => entities == null || entities.isEmpty;
bool get isNoViews => ui == null || ui.isEmpty; bool get isNoViews => ui == null || ui.isEmpty;
@ -79,7 +79,7 @@ class HomeAssistant {
_fetchCompleter.complete(); _fetchCompleter.complete();
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration(); if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
} else { } else {
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")])); _fetchCompleter.completeError(HACException("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")]));
} }
}).catchError((e) { }).catchError((e) {
_fetchCompleter.completeError(e); _fetchCompleter.completeError(e);
@ -122,9 +122,9 @@ class HomeAssistant {
await prefs.setString('cached_panels', json.encode(_rawPanels)); await prefs.setString('cached_panels', json.encode(_rawPanels));
await prefs.setString('cached_services', json.encode(services)); await prefs.setString('cached_services', json.encode(services));
await prefs.setBool('cached', true); await prefs.setBool('cached', true);
} catch (e) { } catch (e, stacktrace) {
await prefs.setBool('cached', false); await prefs.setBool('cached', false);
Logger.e('Error saving cache: $e'); Logger.e('Error saving cache: $e', stacktrace: stacktrace);
} }
Logger.d('Done saving cache'); Logger.d('Done saving cache');
} }
@ -139,38 +139,40 @@ class HomeAssistant {
} }
Future _getConfig(SharedPreferences sharedPrefs) async { Future _getConfig(SharedPreferences sharedPrefs) async {
if (sharedPrefs != null) { _instanceConfig?.clear();
if (sharedPrefs != null && sharedPrefs.containsKey('cached_config')) {
try { try {
var data = json.decode(sharedPrefs.getString('cached_config')); var data = json.decode(sharedPrefs.getString('cached_config'));
_parseConfig(data); _parseConfig(data);
} catch (e) { } catch (e, stacktrace) {
throw HAError("Error getting config: $e"); Logger.e('Error gettong config from cache: $e', stacktrace: stacktrace);
} }
} else { } else {
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) => _parseConfig(data)).catchError((e) { await ConnectionManager().sendSocketMessage(type: "get_config").then((data) => _parseConfig(data)).catchError((e) {
throw HAError("Error getting config: $e"); Logger.e('get_config error: $e');
throw HACException("Error getting config: $e");
}); });
} }
} }
void _parseConfig(data) { void _parseConfig(data) {
_instanceConfig = Map.from(data); _instanceConfig = data;
Logger.d('stream: ${_instanceConfig['components'].contains('stream')}');
} }
Future _getStates(SharedPreferences sharedPrefs) async { Future _getStates(SharedPreferences sharedPrefs) async {
if (sharedPrefs != null) { if (sharedPrefs != null && sharedPrefs.containsKey('cached_states')) {
try { try {
var data = json.decode(sharedPrefs.getString('cached_states')); var data = json.decode(sharedPrefs.getString('cached_states'));
_parseStates(data); _parseStates(data);
} catch (e) { } catch (e, stacktrace) {
throw HAError("Error getting states: $e"); Logger.e('Error getting cached states: $e', stacktrace: stacktrace);
} }
} else { } else {
await ConnectionManager().sendSocketMessage(type: "get_states").then( await ConnectionManager().sendSocketMessage(type: "get_states").then(
(data) => _parseStates(data) (data) => _parseStates(data)
).catchError((e) { ).catchError((e) {
throw HAError("Error getting states: $e"); Logger.e('get_states error: $e');
throw HACException("Error getting states: $e");
}); });
} }
} }
@ -181,7 +183,7 @@ class HomeAssistant {
} }
Future _getLovelace(SharedPreferences sharedPrefs) { Future _getLovelace(SharedPreferences sharedPrefs) {
if (sharedPrefs != null) { if (sharedPrefs != null && sharedPrefs.containsKey('cached_lovelace')) {
try { try {
var data = json.decode(sharedPrefs.getString('cached_lovelace')); var data = json.decode(sharedPrefs.getString('cached_lovelace'));
_rawLovelaceData = data; _rawLovelaceData = data;
@ -204,12 +206,13 @@ class HomeAssistant {
_rawLovelaceData = data; _rawLovelaceData = data;
completer.complete(); completer.complete();
}).catchError((e) { }).catchError((e) {
if ("$e" == "config_not_found") { if ("$e".contains("config_not_found")) {
autoUi = true; autoUi = true;
_rawLovelaceData = null; _rawLovelaceData = null;
completer.complete(); completer.complete();
} else { } else {
completer.completeError(HAError("Error getting lovelace config: $e")); Logger.e('lovelace/config error: $e');
completer.completeError(HACException("Error getting lovelace config: $e"));
} }
}); });
return completer.future; return completer.future;
@ -217,16 +220,17 @@ class HomeAssistant {
} }
Future _getServices(SharedPreferences prefs) async { Future _getServices(SharedPreferences prefs) async {
if (prefs != null) { services?.clear();
if (prefs != null && prefs.containsKey('cached_services')) {
try { try {
var data = json.decode(prefs.getString('cached_services')); var data = json.decode(prefs.getString('cached_services'));
_parseServices(data); _parseServices(data);
} catch (e) { } catch (e, stacktrace) {
Logger.w("Can't get services: $e"); Logger.e(e, stacktrace: stacktrace);
} }
} }
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) { await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
Logger.w("Can't get services: $e"); Logger.e('get_services error: $e');
}); });
} }
@ -236,9 +240,19 @@ class HomeAssistant {
Future _getUserInfo(SharedPreferences sharedPrefs) async { Future _getUserInfo(SharedPreferences sharedPrefs) async {
_userName = null; _userName = null;
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) { if (sharedPrefs != null && sharedPrefs.containsKey('cached_user')) {
Logger.w("Can't get user info: $e"); try {
}); var data = json.decode(sharedPrefs.getString('cached_user'));
_parseUserInfo(data);
} catch (e, stacktrace) {
Logger.e('Error getting cached user info: $e', stacktrace: stacktrace);
}
return Future.value();
} else {
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
Logger.e('auth/current_user error: $e');
});
}
} }
void _parseUserInfo(data) { void _parseUserInfo(data) {
@ -247,23 +261,26 @@ class HomeAssistant {
} }
Future _getPanels(SharedPreferences sharedPrefs) async { Future _getPanels(SharedPreferences sharedPrefs) async {
if (sharedPrefs != null) { if (sharedPrefs != null && sharedPrefs.containsKey('cached_panels')) {
try { try {
var data = json.decode(sharedPrefs.getString('cached_panels')); var data = json.decode(sharedPrefs.getString('cached_panels'));
_parsePanels(data); _parsePanels(data);
} catch (e) { } catch (e, stacktrace) {
throw HAError("Error getting panels list: $e"); Logger.e(e, stacktrace: stacktrace);
panels.clear();
} }
} else { } else {
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) => _parsePanels(data)).catchError((e) { await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) => _parsePanels(data)).catchError((e, stacktrace) {
throw HAError("Error getting panels list: $e"); Logger.e('get_panels error: $e', stacktrace: stacktrace);
panels.clear();
throw HACException('Can\'t get panels: $e');
}); });
} }
} }
void _parsePanels(data) { void _parsePanels(data) {
_rawPanels = data;
panels.clear(); panels.clear();
_rawPanels = data;
List<Panel> dashboards = []; List<Panel> dashboards = [];
data.forEach((k,v) { data.forEach((k,v) {
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}"; String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
@ -316,7 +333,6 @@ class HomeAssistant {
} }
void _handleEntityStateChange(Map eventData) { void _handleEntityStateChange(Map eventData) {
//TheLogger.debug( "New state for ${eventData['entity_id']}");
if (_fetchCompleter != null && _fetchCompleter.isCompleted) { if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
Map data = Map.from(eventData); Map data = Map.from(eventData);
eventBus.fire(new StateChangedEvent( eventBus.fire(new StateChangedEvent(
@ -335,57 +351,5 @@ class HomeAssistant {
void _createUI() { void _createUI() {
Logger.d("Creating Lovelace UI"); Logger.d("Creating Lovelace UI");
ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData); ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData);
/*if (isServiceExist('zha_map')) {
panels.add(
Panel(
id: 'haclient_zha',
componentName: 'haclient_zha',
title: 'ZHA',
urlPath: '/haclient_zha',
icon: 'mdi:zigbee'
)
);
}*/
} }
} }
/*
class SendMessageQueue {
int _messageTimeout;
List<HAMessage> _queue = [];
SendMessageQueue(this._messageTimeout);
void add(String message) {
_queue.add(HAMessage(_messageTimeout, message));
}
List<String> getActualMessages() {
_queue.removeWhere((item) => item.isExpired());
List<String> result = [];
_queue.forEach((haMessage){
result.add(haMessage.message);
});
this.clear();
return result;
}
void clear() {
_queue.clear();
}
}
class HAMessage {
DateTime _timeStamp;
int _messageTimeout;
String message;
HAMessage(this._messageTimeout, this.message) {
_timeStamp = DateTime.now();
}
bool isExpired() {
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
}
}*/

View File

@ -100,6 +100,7 @@ part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.d
part 'entities/vacuum/vacuum_entity.class.dart'; part 'entities/vacuum/vacuum_entity.class.dart';
part 'entities/vacuum/widgets/vacuum_controls.dart'; part 'entities/vacuum/widgets/vacuum_controls.dart';
part 'entities/vacuum/widgets/vacuum_state_button.dart'; part 'entities/vacuum/widgets/vacuum_state_button.dart';
part 'entities/error_entity_widget.dart';
part 'pages/settings/connection_settings.part.dart'; part 'pages/settings/connection_settings.part.dart';
part 'pages/purchase.page.dart'; part 'pages/purchase.page.dart';
part 'pages/widgets/product_purchase.widget.dart'; part 'pages/widgets/product_purchase.widget.dart';
@ -112,8 +113,8 @@ part 'pages/settings/integration_settings.part.dart';
part 'pages/settings/app_settings.page.dart'; part 'pages/settings/app_settings.page.dart';
part 'pages/settings/lookandfeel_settings.part.dart'; part 'pages/settings/lookandfeel_settings.part.dart';
part 'pages/zha_page.dart'; part 'pages/zha_page.dart';
part 'pages/quick_start.page.dart';
part 'home_assistant.class.dart'; part 'home_assistant.class.dart';
part 'pages/log.page.dart';
part 'pages/entity.page.dart'; part 'pages/entity.page.dart';
part 'utils/mdi.class.dart'; part 'utils/mdi.class.dart';
part 'entity_collection.class.dart'; part 'entity_collection.class.dart';
@ -151,19 +152,21 @@ part 'entities/media_player/widgets/media_player_seek_bar.widget.dart';
part 'entities/media_player/widgets/media_player_progress_bar.widget.dart'; part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
part 'pages/whats_new.page.dart'; part 'pages/whats_new.page.dart';
part 'pages/fullscreen.page.dart'; part 'pages/fullscreen.page.dart';
part 'popups.dart';
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = "HA Client"; const String appName = 'HA Client';
const appVersionNumber = "0.8.6"; const appVersionNumber = '1.0.1';
const appVersionAdd = ""; final String appVersionAdd = secrets['version_type'] ?? '';
const appVersion = "$appVersionNumber$appVersionAdd"; final String appVersion = '$appVersionNumber${appVersionAdd.isNotEmpty ? '-' : ''}$appVersionAdd';
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.0.1.md';
Future<void> _reportError(dynamic error, dynamic stackTrace) async { Future<void> _reportError(dynamic error, dynamic stackTrace) async {
// Print the exception to the console. // Print the exception to the console.
if (Logger.isInDebugMode) { if (Logger.isInDebugMode) {
Logger.e('Caught error: $error'); Logger.e('Caught error: $error', skipCrashlytics: true);
Logger.p(stackTrace); Logger.p(stackTrace);
} }
Crashlytics.instance.recordError(error, stackTrace); Crashlytics.instance.recordError(error, stackTrace);
@ -171,11 +174,11 @@ Future<void> _reportError(dynamic error, dynamic stackTrace) async {
} }
void main() async { void main() async {
Crashlytics.instance.enableInDevMode = true; Crashlytics.instance.enableInDevMode = false;
SyncfusionLicense.registerLicense(secrets['syncfusion_license_key']); SyncfusionLicense.registerLicense(secrets['syncfusion_license_key']);
FlutterError.onError = (FlutterErrorDetails details) { FlutterError.onError = (FlutterErrorDetails details) {
Logger.e("Caut Flutter runtime error: ${details.exception}"); Logger.e("Caut Flutter runtime error: ${details.exception}", skipCrashlytics: true);
if (Logger.isInDebugMode) { if (Logger.isInDebugMode) {
FlutterError.dumpErrorToConsole(details); FlutterError.dumpErrorToConsole(details);
} }
@ -235,10 +238,12 @@ class _HAClientAppState extends State<HAClientApp> {
void _handlePurchaseUpdates(purchase) { void _handlePurchaseUpdates(purchase) {
if (purchase is List<PurchaseDetails>) { if (purchase is List<PurchaseDetails>) {
if (purchase[0].status == PurchaseStatus.purchased) { if (purchase[0].status == PurchaseStatus.purchased) {
eventBus.fire(ShowPopupMessageEvent( eventBus.fire(ShowPopupEvent(
popup: Popup(
title: "Thanks a lot!", title: "Thanks a lot!",
body: "Thank you for supporting HA Client development!", body: "Thank you for supporting HA Client development!",
buttonText: "Ok" positiveText: "Ok"
)
)); ));
} else { } else {
Logger.d("Purchase change handler: ${purchase[0].status}"); Logger.d("Purchase change handler: ${purchase[0].status}");
@ -267,7 +272,6 @@ class _HAClientAppState extends State<HAClientApp> {
mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}", mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}", mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
), ),
"/log-view": (context) => LogViewPage(title: "Log"),
"/webview": (context) => standaloneWebview.WebviewScaffold( "/webview": (context) => standaloneWebview.WebviewScaffold(
url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}", url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}",
appBar: new AppBar( appBar: new AppBar(
@ -279,22 +283,26 @@ class _HAClientAppState extends State<HAClientApp> {
), ),
), ),
"/whats-new": (context) => WhatsNewPage(), "/whats-new": (context) => WhatsNewPage(),
"/quick-start": (context) => QuickStartPage(),
"/haclient_zha": (context) => ZhaPage(), "/haclient_zha": (context) => ZhaPage(),
"/auth": (context) => new standaloneWebview.WebviewScaffold( "/auth": (context) => new standaloneWebview.WebviewScaffold(
url: "${ConnectionManager().oauthUrl}", url: "${ConnectionManager().oauthUrl}",
appBar: new AppBar( appBar: new AppBar(
leading: IconButton( leading: IconButton(
icon: Icon(Icons.help), icon: Icon(Icons.help),
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#authentication") onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "https://ha-client.app/help/connection")
), ),
title: new Text("Login with HA"), title: new Text("Login"),
actions: <Widget>[ actions: <Widget>[
FlatButton( FlatButton(
child: Text("Manual", style: Theme.of(context).textTheme.button.copyWith( child: Text("Long-lived token", style: Theme.of(context).textTheme.button.copyWith(
decoration: TextDecoration.underline decoration: TextDecoration.underline
)), )),
onPressed: () { onPressed: () {
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true)); eventBus.fire(ShowPopupEvent(
goBackFirst: true,
popup: TokenLoginPopup()
));
}, },
) )
], ],

View File

@ -31,9 +31,9 @@ class AuthManager {
eventBus.fire(StartAuthEvent(oauthUrl, false)); eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.complete(tempToken); completer.complete(tempToken);
}).catchError((e) { }).catchError((e) {
Logger.e("Error getting temp token: ${e.toString()}"); Logger.e("Error getting temp token: $e");
eventBus.fire(StartAuthEvent(oauthUrl, false)); eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.completeError(HAError("Error getting temp token")); completer.completeError(HACException("Error getting temp token"));
}).whenComplete(() => flutterWebviewPlugin.close()); }).whenComplete(() => flutterWebviewPlugin.close());
} }
}); });

View File

@ -19,6 +19,8 @@ class ConnectionManager {
String _tempToken; String _tempToken;
String oauthUrl; String oauthUrl;
String webhookId; String webhookId;
double haVersion;
String mobileAppDeviceName;
bool settingsLoaded = false; bool settingsLoaded = false;
int appIntegrationVersion; int appIntegrationVersion;
bool get isAuthenticated => _token != null; bool get isAuthenticated => _token != null;
@ -45,35 +47,40 @@ class ConnectionManager {
_port = prefs.getString('hassio-port'); _port = prefs.getString('hassio-port');
webhookId = prefs.getString('app-webhook-id'); webhookId = prefs.getString('app-webhook-id');
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0; appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
mobileAppDeviceName = prefs.getString('app-integration-device-name');
displayHostname = "$_domain:$_port"; displayHostname = "$_domain:$_port";
_webSocketAPIEndpoint = _webSocketAPIEndpoint =
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket"; "${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
httpWebHost = httpWebHost =
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port"; "${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
if ((_domain == null) || (_port == null) || Logger.d('$_domain$_port');
if (_domain == null && _port == null && webhookId == null && mobileAppDeviceName == null) {
completer.completeError(HACNotSetUpException());
stopInit = true;
} else if ((_domain == null) || (_port == null) ||
(_domain.isEmpty) || (_port.isEmpty)) { (_domain.isEmpty) || (_port.isEmpty)) {
completer.completeError(HAError.checkConnectionSettings()); completer.completeError(HACException.checkConnectionSettings());
stopInit = true; stopInit = true;
} else { } else {
final storage = new FlutterSecureStorage(); final storage = new FlutterSecureStorage();
try { try {
_token = await storage.read(key: "hacl_llt"); _token = await storage.read(key: "hacl_llt");
Logger.e("Long-lived token read successful"); Logger.d("Long-lived token read successful");
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent( oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'https://ha-client.app')}&redirect_uri=${Uri 'https://ha-client.app')}&redirect_uri=${Uri
.encodeComponent( .encodeComponent(
'https://ha-client.app/service/auth_callback.html')}"; 'https://ha-client.app/service/auth_callback.html')}";
settingsLoaded = true; settingsLoaded = true;
} catch (e) { } catch (e, stacktrace) {
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()])); completer.completeError(HACException("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
Logger.e("Cannt read secure storage. Need to relogin."); Logger.e("Error reading secure storage: $e", stacktrace: stacktrace);
stopInit = true; stopInit = true;
} }
} }
} else { } else {
if ((_domain == null) || (_port == null) || if ((_domain == null) || (_port == null) ||
(_domain.isEmpty) || (_port.isEmpty)) { (_domain.isEmpty) || (_port.isEmpty)) {
completer.completeError(HAError.checkConnectionSettings()); completer.completeError(HACException.checkConnectionSettings());
stopInit = true; stopInit = true;
} }
} }
@ -106,13 +113,13 @@ class ConnectionManager {
_disconnect().then((_) { _disconnect().then((_) {
if (e is TimeoutException) { if (e is TimeoutException) {
if (connecting != null && !connecting.isCompleted) { if (connecting != null && !connecting.isCompleted) {
connecting.completeError(HAError("Connection timeout")); connecting.completeError(HACException("Connection timeout"));
} }
completer?.completeError(HAError("Connection timeout")); completer?.completeError(HACException("Connection timeout"));
} else if (e is HAError) { } else if (e is HACException) {
completer?.completeError(e); completer?.completeError(e);
} else { } else {
completer?.completeError(HAError("${e.toString()}")); completer?.completeError(HACException("${e.toString()}"));
} }
}); });
}); });
@ -148,7 +155,12 @@ class ConnectionManager {
if (!connecting.isCompleted) connecting.completeError(e); if (!connecting.isCompleted) connecting.completeError(e);
}); });
} else if (data["type"] == "auth_ok") { } else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}"); String v = data["ha_version"];
if (v != null && v.isNotEmpty) {
haVersion = double.tryParse(v.replaceFirst('0.','')) ?? 0;
}
Logger.d("Home assistant version: $v ($haVersion)");
Crashlytics.instance.setString('ha_version', v);
Logger.d("[Connection] Subscribing to events"); Logger.d("[Connection] Subscribing to events");
sendSocketMessage( sendSocketMessage(
type: "subscribe_events", type: "subscribe_events",
@ -166,9 +178,9 @@ class ConnectionManager {
}); });
} else if (data["type"] == "auth_invalid") { } else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}"); Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()])); _messageResolver["auth"]?.completeError(HACException("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
_messageResolver.remove("auth"); _messageResolver.remove("auth");
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")])); if (!connecting.isCompleted) connecting.completeError(HACException("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
} else { } else {
_handleMessage(data); _handleMessage(data);
} }
@ -178,7 +190,7 @@ class ConnectionManager {
onError: (e) => _handleSocketError(e, connecting) onError: (e) => _handleSocketError(e, connecting)
); );
} catch(exeption) { } catch(exeption) {
connecting.completeError(HAError("${exeption.toString()}")); connecting.completeError(HACException("${exeption.toString()}"));
} }
}); });
return connecting.future; return connecting.future;
@ -211,8 +223,8 @@ class ConnectionManager {
//Logger.d("[Received] <== Request id ${data['id']} was successful"); //Logger.d("[Received] <== Request id ${data['id']} was successful");
_messageResolver["${data["id"]}"]?.complete(data["result"]); _messageResolver["${data["id"]}"]?.complete(data["result"]);
} else if (data["id"] != null) { } else if (data["id"] != null) {
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}"); Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}", skipCrashlytics: true);
_messageResolver["${data["id"]}"]?.completeError("${data["error"]["code"]}"); _messageResolver["${data["id"]}"]?.completeError("${data["error"]["code"]}: ${data["error"]["message"]}");
} }
_messageResolver.remove("${data["id"]}"); _messageResolver.remove("${data["id"]}");
} else if (data["type"] == "event") { } else if (data["type"] == "event") {
@ -235,20 +247,20 @@ class ConnectionManager {
_disconnect().then((_) { _disconnect().then((_) {
if (!connectionCompleter.isCompleted) { if (!connectionCompleter.isCompleted) {
isConnected = false; isConnected = false;
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()])); connectionCompleter.completeError(HACException("Disconnected", actions: [HAErrorAction.reconnect()]));
} }
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant"))); eventBus.fire(ShowErrorEvent(HACException("Unable to connect to Home Assistant")));
}); });
} }
void _handleSocketError(e, Completer connectionCompleter) { void _handleSocketError(e, Completer connectionCompleter) {
Logger.e("Socket stream Error: $e"); Logger.e("Socket stream Error: $e", skipCrashlytics: true);
_disconnect().then((_) { _disconnect().then((_) {
if (!connectionCompleter.isCompleted) { if (!connectionCompleter.isCompleted) {
isConnected = false; isConnected = false;
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()])); connectionCompleter.completeError(HACException("Disconnected", actions: [HAErrorAction.reconnect()]));
} }
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant"))); eventBus.fire(ShowErrorEvent(HACException("Unable to connect to Home Assistant")));
}); });
} }
@ -281,7 +293,7 @@ class ConnectionManager {
}); });
}).catchError((e) => completer.completeError(e)); }).catchError((e) => completer.completeError(e));
} else { } else {
completer.completeError(HAError("General login error")); completer.completeError(HACException("General login error"));
} }
return completer.future; return completer.future;
} }
@ -316,7 +328,7 @@ class ConnectionManager {
throw e; throw e;
}); });
}).catchError((e) { }).catchError((e) {
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")])); completer.completeError(HACException("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
}); });
return completer.future; return completer.future;
} }
@ -343,7 +355,7 @@ class ConnectionManager {
_socket.sink.add(rawMessage); _socket.sink.add(rawMessage);
}).catchError((e) { }).catchError((e) {
if (!_completer.isCompleted) { if (!_completer.isCompleted) {
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()])); _completer.completeError(HACException("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
} }
}); });
} else { } else {
@ -372,12 +384,12 @@ class ConnectionManager {
sendHTTPPost( sendHTTPPost(
endPoint: "/api/services/$domain/$service", endPoint: "/api/services/$domain/$service",
data: json.encode(serviceData) data: json.encode(serviceData)
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString()))); ).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HACException(e.toString())));
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData}); //return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
else else
sendHTTPPost( sendHTTPPost(
endPoint: "/api/services/$domain/$service" endPoint: "/api/services/$domain/$service"
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString()))); ).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HACException(e.toString())));
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service}); //return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
return completer.future; return completer.future;
} }

View File

@ -108,40 +108,47 @@ class LocationManager {
} }
updateDeviceLocation() async { updateDeviceLocation() async {
Logger.d("[Foreground location] Started"); try {
Geolocator geolocator = Geolocator(); Logger.d("[Foreground location] Started");
var battery = Battery(); Geolocator geolocator = Geolocator();
String webhookId = ConnectionManager().webhookId; var battery = Battery();
String httpWebHost = ConnectionManager().httpWebHost; String webhookId = ConnectionManager().webhookId;
if (webhookId != null && webhookId.isNotEmpty) { String httpWebHost = ConnectionManager().httpWebHost;
Logger.d("[Foreground location] Getting battery level..."); if (webhookId != null && webhookId.isNotEmpty) {
int batteryLevel = await battery.batteryLevel; Logger.d("[Foreground location] Getting battery level...");
Logger.d("[Foreground location] Getting device location..."); int batteryLevel = await battery.batteryLevel;
Position position = await geolocator.getCurrentPosition( Logger.d("[Foreground location] Getting device location...");
desiredAccuracy: LocationAccuracy.high, Position position = await geolocator.getCurrentPosition(
locationPermissionLevel: GeolocationPermission.locationAlways desiredAccuracy: LocationAccuracy.high,
); locationPermissionLevel: GeolocationPermission.locationAlways
if (position != null) { );
Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})"); if (position != null) {
String url = "$httpWebHost/api/webhook/$webhookId"; Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})");
Map data = { String url = "$httpWebHost/api/webhook/$webhookId";
"type": "update_location", Map data = {
"data": { "type": "update_location",
"gps": [position.latitude, position.longitude], "data": {
"gps_accuracy": position.accuracy, "gps": [position.latitude, position.longitude],
"battery": batteryLevel ?? 100 "gps_accuracy": position.accuracy,
"battery": batteryLevel ?? 100
}
};
Logger.d("[Foreground location] Sending data home...");
http.Response response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(data)
);
if (response.statusCode >= 300) {
Logger.e('Foreground location update error: ${response.body}');
} }
}; Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
Logger.d("[Foreground location] Sending data home..."); } else {
var response = await http.post( Logger.d("[Foreground location] No location. Aborting.");
url, }
headers: {"Content-Type": "application/json"}, }
body: json.encode(data) } catch (e, stack) {
); Logger.e('Foreground location error: ${e.toSTring()}', stacktrace: stack);
Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
} else {
Logger.d("[Foreground location] No location. Aborting.");
}
} }
} }

View File

@ -16,20 +16,31 @@ class MobileAppIntegrationManager {
} }
}; };
static Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) { static String getDefaultDeviceName() {
if (HomeAssistant().userName.isEmpty) {
return '${DeviceInfoManager().model}';
}
return '${HomeAssistant().userName}\'s ${DeviceInfoManager().model}';
}
static Future checkAppRegistration() {
Completer completer = Completer(); Completer completer = Completer();
_appRegistrationData["device_name"] = "${HomeAssistant().userName}'s ${DeviceInfoManager().model}"; _appRegistrationData["device_name"] = ConnectionManager().mobileAppDeviceName ?? getDefaultDeviceName();
(_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}"; (_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
if (ConnectionManager().webhookId == null || forceRegister) { if (ConnectionManager().webhookId == null) {
Logger.d("Mobile app was not registered yet or need to be reseted. Registering..."); Logger.d("Mobile app was not registered yet. Registering...");
var registrationData = Map.from(_appRegistrationData); var registrationData = Map.from(_appRegistrationData);
registrationData.addAll({ registrationData.addAll({
"device_id": "${DeviceInfoManager().unicDeviceId}",
"app_id": "ha_client", "app_id": "ha_client",
"app_name": "$appName", "app_name": "$appName",
"os_name": DeviceInfoManager().osName, "os_name": DeviceInfoManager().osName,
"supports_encryption": false, "supports_encryption": false,
}); });
if (ConnectionManager().haVersion >= 104) {
registrationData.addAll({
"device_id": "${DeviceInfoManager().unicDeviceId}"
});
}
ConnectionManager().sendHTTPPost( ConnectionManager().sendHTTPPost(
endPoint: "/api/mobile_app/registrations", endPoint: "/api/mobile_app/registrations",
includeAuthHeader: true, includeAuthHeader: true,
@ -44,19 +55,26 @@ class MobileAppIntegrationManager {
prefs.setInt('app-integration-version', INTEGRATION_VERSION); prefs.setInt('app-integration-version', INTEGRATION_VERSION);
completer.complete(); completer.complete();
eventBus.fire(ShowPopupDialogEvent( eventBus.fire(ShowPopupEvent(
title: "Mobile app Integration was created", popup: Popup(
body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant", title: "Mobile app Integration was created",
positiveText: "Restart now", body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant",
negativeText: "Later", positiveText: "Restart now",
onPositive: () { negativeText: "Later",
ConnectionManager().callService(domain: "homeassistant", service: "restart"); onPositive: () {
}, ConnectionManager().callService(domain: "homeassistant", service: "restart");
},
)
)); ));
}); });
}).catchError((e) { }).catchError((e) {
completer.complete(); completer.complete();
Logger.e("Error registering the app: ${e.toString()}"); if (e is http.Response) {
Logger.e("Error registering the app: ${e.statusCode}: ${e.body}");
} else {
Logger.e("Error registering the app: ${e?.toString()}");
}
_showError();
}); });
return completer.future; return completer.future;
} else { } else {
@ -77,7 +95,7 @@ class MobileAppIntegrationManager {
registrationData = null; registrationData = null;
} }
if (registrationData == null || registrationData.isEmpty) { if (registrationData == null || registrationData.isEmpty) {
Logger.d("No registration data in response. MobileApp integration was removed or broken"); Logger.w("No registration data in response. MobileApp integration was removed or broken");
_askToRegisterApp(); _askToRegisterApp();
} else { } else {
if (INTEGRATION_VERSION > ConnectionManager().appIntegrationVersion) { if (INTEGRATION_VERSION > ConnectionManager().appIntegrationVersion) {
@ -85,35 +103,19 @@ class MobileAppIntegrationManager {
_askToRemoveAndRegisterApp(); _askToRemoveAndRegisterApp();
} else { } else {
Logger.d('App registration works fine'); 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(); completer.complete();
}).catchError((e) { }).catchError((e) {
if (e is http.Response && e.statusCode == 410) { if (e is http.Response && e.statusCode == 410) {
Logger.e("MobileApp integration was removed"); Logger.w("MobileApp integration was removed");
_askToRegisterApp(); _askToRegisterApp();
} else if (e is http.Response) {
Logger.w("Error updating app registration: ${e.statusCode}: ${e.body}");
_showError();
} else { } else {
Logger.e("Error updating app registration: ${e.toString()}"); Logger.w("Error updating app registration: ${e?.toString()}");
eventBus.fire(ShowPopupDialogEvent( _showError();
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(); completer.complete();
}); });
@ -121,31 +123,43 @@ class MobileAppIntegrationManager {
} }
} }
static void _showError() {
eventBus.fire(ShowPopupEvent(
popup: Popup(
title: "App integration is not working properly",
body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue. You can try to remove Mobile App integration from Home Assistant and restart server to fix this issue.",
positiveText: "Report to GitHub",
negativeText: "Report to Discord",
onPositive: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
},
onNegative: () {
Launcher.launchURLInBrowser("https://discord.gg/u9vq7QE");
},
)
));
}
static void _askToRemoveAndRegisterApp() { static void _askToRemoveAndRegisterApp() {
eventBus.fire(ShowPopupDialogEvent( eventBus.fire(ShowPopupEvent(
title: "Mobile app integration needs to be updated", popup: Popup(
body: "You need to update HA Client integration to continue using notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant and restart Home Assistant. Then go back to HA Client to create app integration again.", title: "Mobile app integration needs to be updated",
positiveText: "Ok", body: "You need to update HA Client integration to continue using notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant and restart Home Assistant. Then go back to HA Client to create app integration again.",
negativeText: "Report an issue", positiveText: "Ok",
onNegative: () { negativeText: "Report an issue",
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new"); onNegative: () {
}, Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
},
)
)); ));
} }
static void _askToRegisterApp() { static void _askToRegisterApp() {
eventBus.fire(ShowPopupDialogEvent( eventBus.fire(ShowPopupEvent(
title: "App integration is broken", popup: RegisterAppPopup(
body: "Looks like app integration was removed from your Home Assistant or it needs to be updated. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant before registering and restart Home Assistant. Then go back here.", title: "Mobile App integration is missing",
positiveText: "Register now", body: "Looks like mobile app integration was removed from your Home Assistant or it needs to be updated.",
negativeText: "Cancel", )
onPositive: () {
SharedPreferences.getInstance().then((prefs) {
prefs.remove("app-webhook-id");
ConnectionManager().webhookId = null;
checkAppRegistration();
});
},
)); ));
} }

View File

@ -14,13 +14,13 @@ class StartupUserMessagesManager {
bool _supportAppDevelopmentMessageShown; bool _supportAppDevelopmentMessageShown;
bool _whatsNewMessageShown; bool _whatsNewMessageShown;
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3"; static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
static final _whatsNewMessageKey = "user-message-shown-whats-new-894"; static final _whatsNewMessageKey = "user-msg-whats-new-url";
void checkMessagesToShow() async { void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload(); await prefs.reload();
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false; _supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
_whatsNewMessageShown = prefs.getBool(_whatsNewMessageKey) ?? false; _whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl;
if (!_whatsNewMessageShown) { if (!_whatsNewMessageShown) {
_showWhatsNewMessage(); _showWhatsNewMessage();
} else if (!_supportAppDevelopmentMessageShown) { } else if (!_supportAppDevelopmentMessageShown) {
@ -29,7 +29,8 @@ class StartupUserMessagesManager {
} }
void _showSupportAppDevelopmentMessage() { void _showSupportAppDevelopmentMessage() {
eventBus.fire(ShowPopupDialogEvent( eventBus.fire(ShowPopupEvent(
popup: Popup(
title: "Hi!", 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.", 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", positiveText: "Show options",
@ -45,12 +46,13 @@ class StartupUserMessagesManager {
prefs.setBool(_supportAppDevelopmentMessageKey, true); prefs.setBool(_supportAppDevelopmentMessageKey, true);
}); });
} }
)
)); ));
} }
void _showWhatsNewMessage() { void _showWhatsNewMessage() {
SharedPreferences.getInstance().then((prefs) { SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_whatsNewMessageKey, true); prefs.setString(_whatsNewMessageKey, whatsNewUrl);
eventBus.fire(ShowPageEvent(path: "/whats-new")); eventBus.fire(ShowPageEvent(path: "/whats-new"));
}); });
} }

View File

@ -19,7 +19,7 @@ class HAClientTheme {
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
letterSpacing: 1, letterSpacing: 1,
), ),
button: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), button: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
); );
static const offEntityStates = [ static const offEntityStates = [

View File

@ -35,7 +35,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
} }
_getEntity() { _getEntity() {
_entity = HomeAssistant().entities.get(widget.entityId); _entity = HomeAssistant().entities?.get(widget.entityId);
} }
@override @override

View File

@ -1,52 +0,0 @@
part of '../main.dart';
class LogViewPage extends StatefulWidget {
LogViewPage({Key key, this.title}) : super(key: key);
final String title;
@override
_LogViewPageState createState() => new _LogViewPageState();
}
class _LogViewPageState extends State<LogViewPage> {
String _logData;
@override
void initState() {
super.initState();
_loadLog();
}
_loadLog() async {
_logData = Logger.getLog();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: new Text(widget.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.content_copy),
onPressed: () {
Clipboard.setData(new ClipboardData(text: _logData));
},
)
],
),
body: TextField(
maxLines: null,
controller: TextEditingController(
text: _logData
),
)
);
}
}

View File

@ -18,12 +18,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
StreamSubscription _showEntityPageSubscription; StreamSubscription _showEntityPageSubscription;
StreamSubscription _showErrorSubscription; StreamSubscription _showErrorSubscription;
StreamSubscription _startAuthSubscription; StreamSubscription _startAuthSubscription;
StreamSubscription _showPopupDialogSubscription; StreamSubscription _showPopupSubscription;
StreamSubscription _showPopupMessageSubscription;
StreamSubscription _reloadUISubscription; StreamSubscription _reloadUISubscription;
StreamSubscription _fullReloadSubscription; StreamSubscription _fullReloadSubscription;
StreamSubscription _showPageSubscription; StreamSubscription _showPageSubscription;
BottomInfoBarController _bottomInfoBarController; BottomInfoBarController _bottomInfoBarController;
bool _popupShown = false;
int _previousViewCount; int _previousViewCount;
bool _showLoginButton = false; bool _showLoginButton = false;
bool _preventAppRefresh = false; bool _preventAppRefresh = false;
@ -105,7 +105,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
StartupUserMessagesManager().checkMessagesToShow(); StartupUserMessagesManager().checkMessagesToShow();
}); });
}, onError: (e) { }, onError: (e) {
_setErrorState(e); if (e is HACNotSetUpException) {
Navigator.of(context).pushReplacementNamed('/quick-start');
} else {
_setErrorState(e);
}
}); });
}); });
} }
@ -131,10 +135,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}); });
HomeAssistant().saveCache(); HomeAssistant().saveCache();
}).catchError((e) { }).catchError((e) {
if (e is HAError) { if (e is HACException) {
_setErrorState(e); _setErrorState(e);
} else { } else {
_setErrorState(HAError(e.toString())); _setErrorState(HACException(e.toString()));
} }
}); });
eventBus.fire(RefreshDataFinishedEvent()); eventBus.fire(RefreshDataFinishedEvent());
@ -178,27 +182,17 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_fullLoad(); _fullLoad();
}); });
} }
if (_showPopupDialogSubscription == null) { if (_showPopupSubscription == null) {
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().listen((event){ _showPopupSubscription = eventBus.on<ShowPopupEvent>().listen((event){
_showPopupDialog( if (!_popupShown) {
title: event.title, _popupShown = true;
body: event.body, if (event.goBackFirst) {
onPositive: event.onPositive, Navigator.of(context).pop();
onNegative: event.onNegative, }
positiveText: event.positiveText, event.popup.show(context).then((_){
negativeText: event.negativeText _popupShown = false;
); });
}); }
}
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) { if (_serviceCallSubscription == null) {
@ -211,7 +205,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
if (_showEntityPageSubscription == null) { if (_showEntityPageSubscription == null) {
_showEntityPageSubscription = _showEntityPageSubscription =
eventBus.on<ShowEntityPageEvent>().listen((event) { eventBus.on<ShowEntityPageEvent>().listen((event) {
_showEntityPage(event.entity?.entityId); Logger.d('Showing entity page event handled: ${event.entityId}');
_showEntityPage(event.entityId);
}); });
} }
@ -254,53 +249,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl}); Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
} }
_setErrorState(HAError e) { _setErrorState(HACException e) {
if (e == null) { if (e == null) {
_bottomInfoBarController.showErrorBottomBar( _bottomInfoBarController.showErrorBottomBar(
HAError("Unknown error") HACException("Unknown error")
); );
} else { } else {
_bottomInfoBarController.showErrorBottomBar(e); _bottomInfoBarController.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,
);
},
);
}
void _notifyServiceCalled(String domain, String service, entityId) { void _notifyServiceCalled(String domain, String service, entityId) {
_bottomInfoBarController.showInfoBottomBar( _bottomInfoBarController.showInfoBottomBar(
message: "Calling $domain.$service", message: "Calling $domain.$service",
@ -309,6 +267,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
void _showEntityPage(String entityId) { void _showEntityPage(String entityId) {
Logger.d('Showing entity page: $entityId');
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -384,20 +343,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
]); ]);
menuItems.addAll([ menuItems.addAll([
Divider(), 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( new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")), leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
title: Text("Report an issue"), title: Text("Report an issue"),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new"); Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
}, },
), ),
Divider(), Divider(),
@ -415,7 +366,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
title: Text("Help"), title: Text("Help"),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Launcher.launchURL("http://ha-client.app/docs"); Launcher.launchURLInCustomTab(
context: context,
url: "http://ha-client.app/help"
);
}, },
), ),
new ListTile( new ListTile(
@ -423,7 +377,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
title: Text("Contacts/Discussion"), title: Text("Contacts/Discussion"),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Launcher.launchURL("https://discord.gg/nd6FZQ"); Launcher.launchURLInBrowser("https://discord.gg/u9vq7QE");
}, },
), ),
new ListTile( new ListTile(
@ -438,7 +392,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Launcher.launchURL("http://ha-client.app/"); Launcher.launchURLInBrowser("http://ha-client.app/");
}, },
child: Text( child: Text(
"ha-client.app", "ha-client.app",
@ -580,9 +534,23 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
FlatButton( FlatButton(
child: Text("Login with Home Assistant", style: Theme.of(context).textTheme.button), child: Text("Login", style: Theme.of(context).textTheme.button),
color: Colors.blue, color: Theme.of(context).primaryColor,
onPressed: () => _fullLoad(), onPressed: () => _fullLoad(),
),
Container(height: 20,),
FlatButton(
child: Text("Login with long-lived token", style: Theme.of(context).textTheme.button),
color: Theme.of(context).primaryColor,
onPressed: () => eventBus.fire(ShowPopupEvent(
popup: TokenLoginPopup()
))
),
Container(height: 20,),
FlatButton(
child: Text("Connection settings", style: Theme.of(context).textTheme.button),
color: Theme.of(context).primaryColor,
onPressed: () => Navigator.of(context).pushNamed('/connection-settings')
) )
] ]
) )
@ -614,13 +582,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
icon: mediaMenuIcon, icon: mediaMenuIcon,
onPressed: () { onPressed: () {
showMenu( showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100.0, 50, 0.0), position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, MediaQuery.of(context).padding.top + Sizes.iconSize + 20, 50, 0),
context: context, context: context,
items: mediaMenuItems items: mediaMenuItems
).then((String val) { ).then((String val) {
if (val == "play_media") { if (val == "play_media") {
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""}); Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
} else { } else if (val != null) {
_showEntityPage(val); _showEntityPage(val);
} }
}); });
@ -631,7 +599,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
"mdi:dots-vertical"), color: Colors.white,), "mdi:dots-vertical"), color: Colors.white,),
onPressed: () { onPressed: () {
showMenu( showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100, 0.0, 0.0), position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, MediaQuery.of(context).padding.top + Sizes.iconSize + 20, 0.0, 0.0),
context: context, context: context,
items: serviceMenuItems items: serviceMenuItems
).then((String val) { ).then((String val) {
@ -704,8 +672,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_lovelaceSubscription?.cancel(); _lovelaceSubscription?.cancel();
_settingsSubscription?.cancel(); _settingsSubscription?.cancel();
_serviceCallSubscription?.cancel(); _serviceCallSubscription?.cancel();
_showPopupDialogSubscription?.cancel(); _showPopupSubscription?.cancel();
_showPopupMessageSubscription?.cancel();
_showEntityPageSubscription?.cancel(); _showEntityPageSubscription?.cancel();
_showErrorSubscription?.cancel(); _showErrorSubscription?.cancel();
_startAuthSubscription?.cancel(); _startAuthSubscription?.cancel();

View File

@ -105,7 +105,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
); );
HomeAssistant().sendFromPlayerId = null; HomeAssistant().sendFromPlayerId = null;
} }
eventBus.fire(ShowEntityPageEvent(entity: entity)); eventBus.fire(ShowEntityPageEvent(entityId: entity.entityId));
} }
} }

View File

@ -0,0 +1,48 @@
part of '../main.dart';
class QuickStartPage extends StatefulWidget {
QuickStartPage({Key key, this.title}) : super(key: key);
final String title;
@override
_QuickStartPageState createState() => new _QuickStartPageState();
}
class _QuickStartPageState extends State<QuickStartPage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
leading: Container(),
title: Text('Quick start'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.help),
onPressed: () {
Launcher.launchURLInCustomTab(
context: context,
url: 'https://ha-client.app/help'
);
},
)
],
),
body: ConnectionSettingsPage(
quickStart: true,
)
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -35,12 +35,15 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
} }
Widget _buildMenu(BuildContext context) { Widget _buildMenu(BuildContext context) {
List<Widget> items = [
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings),
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel),
];
if (ConnectionManager().webhookId != null) {
items.insert(1, _buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings));
}
return ListView( return ListView(
children: <Widget>[ children: items,
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings),
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings),
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel),
],
); );
} }
@ -48,25 +51,30 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget section; Widget section;
String title; String title;
String helpUrl;
switch (_currentSection) { switch (_currentSection) {
case AppSettingsSection.menu: { case AppSettingsSection.menu: {
section = _buildMenu(context); section = _buildMenu(context);
title = 'App settings'; title = 'App settings';
helpUrl = 'https://ha-client.app/help/';
break; break;
} }
case AppSettingsSection.connectionSettings: { case AppSettingsSection.connectionSettings: {
section = ConnectionSettingsPage(); section = ConnectionSettingsPage();
title = 'App settings - Connection'; title = 'App settings - Connection';
helpUrl = 'https://ha-client.app/help/connection';
break; break;
} }
case AppSettingsSection.integrationSettings: { case AppSettingsSection.integrationSettings: {
section = IntegrationSettingsPage(); section = IntegrationSettingsPage();
title = 'App settings - Integration'; title = 'App settings - Integration';
helpUrl = 'https://ha-client.app/help/mobile_app_integration';
break; break;
} }
case AppSettingsSection.lookAndFeel: { case AppSettingsSection.lookAndFeel: {
section = LookAndFeelSettingsPage(); section = LookAndFeelSettingsPage();
title = 'App settings - Look&Feel'; title = 'App settings - Look&Feel';
helpUrl = 'https://ha-client.app/help/';
break; break;
} }
default: default:
@ -86,6 +94,17 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
} }
}), }),
title: Text(title), title: Text(title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.help),
onPressed: () {
Launcher.launchURLInCustomTab(
context: context,
url: helpUrl
);
},
)
],
), ),
body: section body: section
), ),

View File

@ -1,187 +1,194 @@
part of '../../main.dart'; part of '../../main.dart';
class ConnectionSettingsPage extends StatefulWidget { class ConnectionSettingsPage extends StatefulWidget {
ConnectionSettingsPage({Key key, this.title}) : super(key: key); ConnectionSettingsPage({Key key, this.title, this.quickStart: false}) : super(key: key);
final String title; final String title;
final bool quickStart;
@override @override
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState(); _ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
} }
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _hassioDomain = ""; String _homeAssistantUrl = '';
String _newHassioDomain = ""; String _deviceName;
String _hassioPort = ""; bool _loaded = false;
String _newHassioPort = ""; bool _includeDeviceName = false;
String _socketProtocol = "wss";
String _newSocketProtocol = "wss";
String _longLivedToken = "";
String _newLongLivedToken = "";
String oauthUrl; final _formKey = GlobalKey<FormState>();
bool useOAuth = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadSettings(); if (!widget.quickStart) {
_loadSettings();
} else {
_deviceName = MobileAppIntegrationManager.getDefaultDeviceName();
_includeDeviceName = true;
_loaded = true;
}
} }
_loadSettings() async { _loadSettings() async {
_includeDeviceName = widget.quickStart || ConnectionManager().webhookId == null;
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage(); String domain = prefs.getString('hassio-domain')?? '';
String port = prefs.getString('hassio-port') ?? '';
try { String urlProtocol = prefs.getString('hassio-res-protocol') ?? 'https';
useOAuth = prefs.getBool("oauth-used") ?? true; _homeAssistantUrl = '$urlProtocol://$domain:$port';
} catch (e) { _deviceName = prefs.getString('app-integration-device-name') ?? MobileAppIntegrationManager.getDefaultDeviceName();
useOAuth = true;
}
if (!useOAuth) {
try {
_longLivedToken = _newLongLivedToken =
await storage.read(key: "hacl_llt");
} catch (e) {
_longLivedToken = _newLongLivedToken = "";
await storage.delete(key: "hacl_llt");
}
}
setState(() { setState(() {
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? ""; _loaded = true;
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
}); });
} }
bool _checkConfigChanged() {
return (
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
(_newLongLivedToken != _longLivedToken));
}
_saveSettings() async { _saveSettings() async {
_newHassioDomain = _newHassioDomain.trim(); String socketProtocol;
if (_newHassioDomain.startsWith("http") && _newHassioDomain.indexOf("//") > 0) { String domain;
_newHassioDomain.startsWith("https") ? _newSocketProtocol = "wss" : _newSocketProtocol = "ws"; String port;
_newHassioDomain = _newHassioDomain.split("//")[1]; if (_homeAssistantUrl.startsWith("http") && _homeAssistantUrl.indexOf("//") > 0) {
_homeAssistantUrl.startsWith("https") ? socketProtocol = "wss" : socketProtocol = "ws";
domain = _homeAssistantUrl.split("//")[1];
} else {
domain = _homeAssistantUrl;
socketProtocol = "ws";
} }
_newHassioDomain = _newHassioDomain.split("/")[0]; domain = domain.split("/")[0];
if (_newHassioDomain.contains(":")) { if (domain.contains(":")) {
List<String> domainAndPort = _newHassioDomain.split(":"); List<String> domainAndPort = domain.split(":");
_newHassioDomain = domainAndPort[0]; domain = domainAndPort[0];
_newHassioPort = domainAndPort[1]; port = domainAndPort[1];
} }
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage(); await prefs.setString("hassio-domain", domain);
if (_newLongLivedToken.isNotEmpty) { if (port == null || port.isEmpty) {
_newLongLivedToken = _newLongLivedToken.trim(); port = socketProtocol == "wss" ? "443" : "80";
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 { } else {
_newHassioPort = _newHassioPort.trim(); port = port.trim();
}
await prefs.setString("hassio-port", port);
await prefs.setString("hassio-protocol", socketProtocol);
await prefs.setString("hassio-res-protocol", socketProtocol == "wss" ? "https" : "http");
if (_includeDeviceName) {
await prefs.setString('app-integration-device-name', _deviceName);
} }
prefs.setString("hassio-port", _newHassioPort);
prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( if (!_loaded) {
scrollDirection: Axis.vertical, return PageLoadingIndicator();
padding: const EdgeInsets.all(20.0), }
children: <Widget>[ List<Widget> formChildren = <Widget>[
Text(
"Home Assistant url:",
style: Theme.of(context).textTheme.headline,
),
TextFormField(
initialValue: _homeAssistantUrl,
decoration: InputDecoration(
hintText: "Please enter url",
contentPadding: EdgeInsets.all(0),
hintStyle: Theme.of(context).textTheme.subhead.copyWith(
color: Theme.of(context).textTheme.overline.color
)
),
onSaved: (newValue) {
_homeAssistantUrl = newValue.trim();
},
validator: (value) {
if (value.trim().isEmpty) {
return 'Url is required';
}
return null;
},
),
Container(
height: 10,
),
Text(
"For example:",
style: Theme.of(context).textTheme.body1,
),
Text(
"192.186.2.14:8123",
style: Theme.of(context).textTheme.subhead,
),
Text(
"http://myhome.duckdns.org:8123",
style: Theme.of(context).textTheme.subhead,
),
Text(
"https://efkmfrwk3r4fsfwrfrg5.ui.nabu.casa/",
style: Theme.of(context).textTheme.subhead,
),
];
if (_includeDeviceName) {
formChildren.addAll(<Widget>[
Container(
height: 30,
),
Text( Text(
"Connection settings", "Device name:",
style: Theme.of(context).textTheme.headline, style: Theme.of(context).textTheme.headline,
), ),
new Row( TextFormField(
children: [ initialValue: _deviceName,
Text("Use ssl (HTTPS)"), onSaved: (newValue) {
Switch( _deviceName = newValue.trim();
value: (_newSocketProtocol == "wss"), },
onChanged: (value) { decoration: InputDecoration(
setState(() { hintText: 'Please enter device name',
_newSocketProtocol = value ? "wss" : "ws"; contentPadding: EdgeInsets.all(0),
}); hintStyle: Theme.of(context).textTheme.subhead.copyWith(
}, color: Theme.of(context).textTheme.overline.color
) )
],
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant domain or ip address"
), ),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)), validator: (value) {
onChanged: (value) { if (value.trim().isEmpty) {
_newHassioDomain = value; return 'Device name is required';
}
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant port (default is 8123)"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
onChanged: (value) {
_newHassioPort = value;
}
),
new Text(
"Try ports 80 and 443 if default is not working and you don't know why.",
style: Theme.of(context).textTheme.caption,
),
Text(
"Authentication settings",
style: Theme.of(context).textTheme.headline,
),
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: Theme.of(context).textTheme.body1.copyWith(
color: Colors.redAccent
),
),
new TextField(
decoration: InputDecoration(
labelText: "Long-lived token"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
onChanged: (value) {
_newLongLivedToken = value;
} }
return null;
},
), ),
Container( ]);
height: Sizes.rowPadding, }
),
RaisedButton( formChildren.addAll(<Widget>[
child: Text('Apply', style: Theme.of(context).textTheme.button), Container(
color: Theme.of(context).primaryColorDark, height: 30,
),
ButtonTheme(
height: 60,
child: RaisedButton(
child: Text(widget.quickStart ? 'Engage' : 'Apply', style: Theme.of(context).textTheme.button.copyWith(fontSize: 20)),
color: Theme.of(context).primaryColor,
onPressed: () { onPressed: () {
if (_checkConfigChanged()) { if (_formKey.currentState.validate()) {
Logger.d("Settings changed. Saving..."); _formKey.currentState.save();
_saveSettings().then((r) { _saveSettings().then((r) {
Navigator.pop(context); if (widget.quickStart) {
Navigator.pushReplacementNamed(context, '/');
} else {
Navigator.pop(context);
}
eventBus.fire(SettingsChangedEvent(true)); eventBus.fire(SettingsChangedEvent(true));
}); });
} else {
Logger.d("Settings was not changed");
Navigator.pop(context);
} }
}, },
) )
], )
]);
return Form(
key: _formKey,
child: ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: formChildren,
),
); );
} }

View File

@ -36,7 +36,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
}); });
} }
void incLocationInterval() { void _incLocationInterval() {
if (_locationInterval < 720) { if (_locationInterval < 720) {
setState(() { setState(() {
_locationInterval = _locationInterval + 5; _locationInterval = _locationInterval + 5;
@ -44,7 +44,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
} }
} }
void decLocationInterval() { void _decLocationInterval() {
if (_locationInterval > 5) { if (_locationInterval > 5) {
setState(() { setState(() {
_locationInterval = _locationInterval - 5; _locationInterval = _locationInterval - 5;
@ -52,46 +52,6 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
} }
} }
restart() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you want to restart Home Assistant?",
body: "This will restart your Home Assistant server.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "restart");
},
));
}
stop() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you want to STOP Home Assistant?",
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "stop");
},
));
}
updateRegistration() {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
}
resetRegistration() {
eventBus.fire(ShowPopupDialogEvent(
title: "Waaaait",
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
positiveText: "Done it already",
negativeText: "Ok, I will",
onPositive: () {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
},
));
}
_switchLocationTrackingState(bool state) async { _switchLocationTrackingState(bool state) async {
if (state) { if (state) {
await LocationManager().updateDeviceLocation(); await LocationManager().updateDeviceLocation();
@ -146,42 +106,16 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
FlatButton( FlatButton(
padding: EdgeInsets.all(0.0), padding: EdgeInsets.all(0.0),
child: Text("-", style: Theme.of(context).textTheme.title), child: Text("-", style: Theme.of(context).textTheme.title),
onPressed: () => decLocationInterval(), onPressed: () => _decLocationInterval(),
), ),
Text("$_locationInterval", style: Theme.of(context).textTheme.title), Text("$_locationInterval", style: Theme.of(context).textTheme.title),
FlatButton( FlatButton(
padding: EdgeInsets.all(0.0), padding: EdgeInsets.all(0.0),
child: Text("+", style: Theme.of(context).textTheme.title), child: Text("+", style: Theme.of(context).textTheme.title),
onPressed: () => incLocationInterval(), onPressed: () => _incLocationInterval(),
), ),
], ],
), )
Divider(),
Text("Integration status", style: Theme.of(context).textTheme.title),
Container(height: Sizes.rowPadding,),
Text(
"${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}",
style: Theme.of(context).textTheme.subtitle,
),
Container(height: 6.0,),
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
//Divider(),
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
RaisedButton(
color: Colors.blue,
onPressed: () => updateRegistration(),
child: Text("Check integration", style: Theme.of(context).textTheme.button)
),
Container(width: 10.0,),
RaisedButton(
color: Colors.redAccent,
onPressed: () => resetRegistration(),
child: Text("Reset integration", style: Theme.of(context).textTheme.button)
)
],
),
] ]
); );
} }

View File

@ -25,7 +25,7 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
await prefs.reload(); await prefs.reload();
SharedPreferences.getInstance().then((prefs) { SharedPreferences.getInstance().then((prefs) {
setState(() { setState(() {
_currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme]; _currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme.index];
}); });
}); });
} }

View File

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

View File

@ -40,12 +40,12 @@ class BottomInfoBarController {
} }
} }
void showErrorBottomBar(HAError error) { void showErrorBottomBar(HACException error) {
actions.clear(); actions.clear();
actions.addAll(error.actions); actions.addAll(error.actions);
bottomBarErrorColor = true; bottomBarErrorColor = true;
bottomBarProgress = false; bottomBarProgress = false;
bottomBarText = "${error.message}"; bottomBarText = "${error.cause}";
if (show == null) { if (show == null) {
initialState = true; initialState = true;
} else { } else {

View File

@ -24,17 +24,11 @@ class Panel {
icon = Panel.iconsByComponent[componentName]; icon = Panel.iconsByComponent[componentName];
} }
isHidden = (componentName == 'kiosk' || componentName == 'states' || componentName == 'profile' || componentName == 'developer-tools'); isHidden = (componentName == 'kiosk' || componentName == 'states' || componentName == 'profile' || componentName == 'developer-tools');
isWebView = (componentName != 'config' && componentName != 'lovelace' && !componentName.startsWith('haclient')); isWebView = (componentName != 'lovelace' && !componentName.startsWith('haclient'));
} }
void handleOpen(BuildContext context) { void handleOpen(BuildContext context) {
if (componentName == "config") { if (componentName.startsWith('haclient')) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PanelPage(title: "$title", panel: this),
)
);
} else if (componentName.startsWith('haclient')) {
Navigator.of(context).pushNamed(urlPath); Navigator.of(context).pushNamed(urlPath);
} else if (componentName == 'lovelace') { } else if (componentName == 'lovelace') {
HomeAssistant().lovelaceDashboardUrl = this.urlPath; HomeAssistant().lovelaceDashboardUrl = this.urlPath;
@ -68,10 +62,6 @@ class Panel {
Widget getWidget() { Widget getWidget() {
switch (componentName) { switch (componentName) {
case "config": {
return ConfigPanelWidget();
}
default: { default: {
return Text("Unsupported panel component: $componentName"); return Text("Unsupported panel component: $componentName");
} }

View File

@ -54,7 +54,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
}); });
} }
}).catchError((e) { }).catchError((e) {
Logger.e("Error loading $entityId history: $e"); Logger.e("Error loading $entityId history: $e", skipCrashlytics: true);
if (!_disposed) { if (!_disposed) {
setState(() { setState(() {
_history = []; _history = [];

213
lib/popups.dart Normal file
View File

@ -0,0 +1,213 @@
part of 'main.dart';
class Popup {
final String title;
final String body;
final String positiveText;
final String negativeText;
final onPositive;
final onNegative;
Popup({@required this.title, @required this.body, this.positiveText, this.negativeText, this.onPositive, this.onNegative});
Future show(BuildContext context) {
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
return 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,
);
},
);
}
}
class TokenLoginPopup extends Popup {
TokenLoginPopup() : super(title: 'Login with long-lived token', body: '');
final _tokenLoginFormKey = GlobalKey<FormState>();
@override
Future show(BuildContext context) {
return showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return SimpleDialog(
title: new Text('Login with long-lived token'),
children: <Widget>[
Form(
key: _tokenLoginFormKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: EdgeInsets.all(20),
child: TextFormField(
onSaved: (newValue) {
final storage = new FlutterSecureStorage();
storage.write(key: "hacl_llt", value: newValue.trim()).then((_) {
Navigator.of(context).pop();
eventBus.fire(SettingsChangedEvent(true));
});
},
decoration: InputDecoration(
hintText: 'Please enter long-lived token',
contentPadding: EdgeInsets.all(0),
hintStyle: Theme.of(context).textTheme.subhead.copyWith(
color: Theme.of(context).textTheme.overline.color
)
),
validator: (value) {
if (value.trim().isEmpty) {
return 'Long-lived token can\'t be emty';
}
return null;
},
)
),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
Container(width: 10),
FlatButton(
child: Text('Login'),
onPressed: () {
if (_tokenLoginFormKey.currentState.validate()) {
_tokenLoginFormKey.currentState.save();
}
},
),
],
)
],
),
)
],
);
},
);
}
}
class RegisterAppPopup extends Popup {
RegisterAppPopup({String title, String body}): super(title: title, body: body);
final _tokenLoginFormKey = GlobalKey<FormState>();
@override
Future show(BuildContext context) {
return showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return SimpleDialog(
title: new Text('${this.title}'),
children: <Widget>[
Form(
key: _tokenLoginFormKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: EdgeInsets.only(left: 20, right: 20),
child: Text('${this.body}')
),
Padding(
padding: EdgeInsets.all(20),
child: TextFormField(
initialValue: ConnectionManager().mobileAppDeviceName ?? MobileAppIntegrationManager.getDefaultDeviceName(),
onSaved: (newValue) {
String deviceName = newValue?.trim();
SharedPreferences.getInstance().then((prefs) {
prefs.remove("app-webhook-id");
prefs.setString('app-integration-device-name', deviceName);
ConnectionManager().webhookId = null;
ConnectionManager().mobileAppDeviceName = deviceName;
Navigator.of(context).pop();
MobileAppIntegrationManager.checkAppRegistration();
});
},
decoration: InputDecoration(
labelText: 'Device name*',
hintText: 'Please enter device name',
contentPadding: EdgeInsets.all(0),
hintStyle: Theme.of(context).textTheme.subhead.copyWith(
color: Theme.of(context).textTheme.overline.color
)
),
validator: (value) {
if (value.trim().isEmpty) {
return 'Device name can\'t be emty';
}
return null;
},
)
),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
Container(width: 10),
FlatButton(
child: Text('Create now'),
onPressed: () {
if (_tokenLoginFormKey.currentState.validate()) {
_tokenLoginFormKey.currentState.save();
}
},
),
],
)
],
),
)
],
);
},
);
}
}

View File

@ -56,30 +56,17 @@ class NotifyServiceCallEvent {
NotifyServiceCallEvent(this.domain, this.service, this.entityId); NotifyServiceCallEvent(this.domain, this.service, this.entityId);
} }
class ShowPopupDialogEvent { class ShowPopupEvent {
final String title; final Popup popup;
final String body; final bool goBackFirst;
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}); ShowPopupEvent({this.popup, this.goBackFirst: false});
}
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 { class ShowEntityPageEvent {
final Entity entity; final String entityId;
ShowEntityPageEvent({this.entity}); ShowEntityPageEvent({@required this.entityId});
} }
class ShowPageEvent { class ShowPageEvent {
@ -90,7 +77,7 @@ class ShowPageEvent {
} }
class ShowErrorEvent { class ShowErrorEvent {
final HAError error; final HACException error;
ShowErrorEvent(this.error); ShowErrorEvent(this.error);
} }

View File

@ -1,21 +1,33 @@
part of '../main.dart'; part of '../main.dart';
class HAError { class HACException implements Exception {
String message; String cause;
final List<HAErrorAction> actions; final List<HAErrorAction> actions;
HAError(this.message, {this.actions: const [HAErrorAction.tryAgain()]}); HACException(this.cause, {this.actions: const [HAErrorAction.tryAgain()]});
HAError.unableToConnect({this.actions = const [HAErrorAction.tryAgain()]}) { HACException.unableToConnect({this.actions = const [HAErrorAction.tryAgain()]}) {
this.message = "Unable to connect to Home Assistant"; this.cause = "Unable to connect to Home Assistant";
} }
HAError.disconnected({this.actions = const [HAErrorAction.reconnect()]}) { HACException.disconnected({this.actions = const [HAErrorAction.reconnect()]}) {
this.message = "Disconnected"; this.cause = "Disconnected";
} }
HAError.checkConnectionSettings({this.actions = const [HAErrorAction.reload(), HAErrorAction(title: "Settings", type: HAErrorActionType.OPEN_CONNECTION_SETTINGS)]}) { HACException.checkConnectionSettings({this.actions = const [HAErrorAction.reload(), HAErrorAction(title: "Settings", type: HAErrorActionType.OPEN_CONNECTION_SETTINGS)]}) {
this.message = "Check connection settings"; this.cause = "Check connection settings";
}
@override
String toString() {
return 'HACException: $cause';
}
}
class HACNotSetUpException implements Exception {
@override
String toString() {
return 'HA Client is not set up';
} }
} }

View File

@ -2,7 +2,7 @@ part of '../main.dart';
class Launcher { class Launcher {
static void launchURL(String url) async { static void launchURLInBrowser(String url) async {
if (await urlLauncher.canLaunch(url)) { if (await urlLauncher.canLaunch(url)) {
await urlLauncher.launch(url); await urlLauncher.launch(url);
} else { } else {
@ -19,7 +19,6 @@ class Launcher {
enableDefaultShare: enableDefaultShare, enableDefaultShare: enableDefaultShare,
enableUrlBarHiding: true, enableUrlBarHiding: true,
showPageTitle: showPageTitle, showPageTitle: showPageTitle,
animation: new CustomTabsAnimation.slideIn(),
extraCustomTabs: <String>[ extraCustomTabs: <String>[
// ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox // ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox
'org.mozilla.firefox', 'org.mozilla.firefox',
@ -31,7 +30,7 @@ class Launcher {
} catch (e) { } catch (e) {
Logger.w("Can't open custom tab: ${e.toString()}"); Logger.w("Can't open custom tab: ${e.toString()}");
Logger.w("Launching in default browser"); Logger.w("Launching in default browser");
Launcher.launchURL(url); Launcher.launchURLInBrowser(url);
} }
} }

View File

@ -1,20 +1,11 @@
import 'package:date_format/date_format.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
enum ErrorLevel {ERROR, WARNING, DEBUG}
class Logger { class Logger {
static List<String> _log = [];
static String getLog() {
String res = '';
_log.forEach((line) {
res += "$line\n";
});
return res;
}
static bool get isInDebugMode { static bool get isInDebugMode {
bool inDebugMode = false; bool inDebugMode = false;
@ -27,26 +18,26 @@ class Logger {
print(data); print(data);
} }
static void e(String message) { static void e(dynamic message, {dynamic stacktrace, bool skipCrashlytics: false}) {
_writeToLog("Error", message); _writeToLog(ErrorLevel.ERROR, message.toString(), stacktrace, skipCrashlytics);
} }
static void w(String message) { static void w(String message) {
_writeToLog("Warning", message); _writeToLog(ErrorLevel.WARNING, message, null, true);
} }
static void d(String message) { static void d(String message) {
_writeToLog("Debug", message); _writeToLog(ErrorLevel.DEBUG, message, null, true);
} }
static void _writeToLog(String level, String message) { static void _writeToLog(ErrorLevel level, String message, dynamic stacktrace, bool skipCrashlytics) {
if (isInDebugMode) { if (isInDebugMode) {
debugPrint('$message'); debugPrint('$message');
} if (stacktrace != null) {
DateTime t = DateTime.now(); debugPrint('$stacktrace');
_log.add("${formatDate(t, ["mm","dd"," ","HH",":","nn",":","ss"])} [$level] : $message"); }
if (_log.length > 100) { } else if (!skipCrashlytics) {
_log.removeAt(0); Crashlytics.instance.recordError('$message', stacktrace);
} }
} }

View File

@ -1,7 +1,7 @@
name: hass_client name: hass_client
description: Home Assistant Android Client description: Home Assistant Android Client
version: 0.8.6+897 version: 1.0.1+1013
environment: environment:
@ -24,12 +24,12 @@ dependencies:
flutter_webview_plugin: ^0.3.10+1 flutter_webview_plugin: ^0.3.10+1
webview_flutter: ^0.3.19+7 webview_flutter: ^0.3.19+7
firebase_messaging: ^6.0.13 firebase_messaging: ^6.0.13
flutter_secure_storage: ^3.3.1+1 flutter_secure_storage: ^3.3.3
device_info: ^0.4.1+4 device_info: ^0.4.1+4
flutter_local_notifications: ^1.1.6 flutter_local_notifications: ^1.1.6
geolocator: ^5.3.1 geolocator: ^5.3.1
workmanager: ^0.2.2 workmanager: ^0.2.2
battery: ^0.3.1+7 battery: ^1.0.0
firebase_crashlytics: ^0.1.3+3 firebase_crashlytics: ^0.1.3+3
syncfusion_flutter_core: ^18.1.43 syncfusion_flutter_core: ^18.1.43
syncfusion_flutter_gauges: ^18.1.43 syncfusion_flutter_gauges: ^18.1.43
@ -38,19 +38,12 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_launcher_icons: any
flutter_icons:
android: true
ios: false
image_path: "images/icon/icon.png"
flutter: flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- images/hassio-192x192.png
- assets/js/externalAuth.js - assets/js/externalAuth.js
- assets/html/cameraView.html - assets/html/cameraView.html
- assets/html/cameraLiveView.html - assets/html/cameraLiveView.html

View File

@ -4,6 +4,7 @@ import 'dart:io';
Future<void> main() async { Future<void> main() async {
final config = { final config = {
'syncfusion_license_key': Platform.environment['SYNCFUSION_LICENSE_KEY'], 'syncfusion_license_key': Platform.environment['SYNCFUSION_LICENSE_KEY'],
'version_type': Platform.environment['HA_CLIENT_VERSION_TYPE'] ?? ''
}; };
final filename = 'lib/.secrets.dart'; final filename = 'lib/.secrets.dart';