Compare commits

...

124 Commits

Author SHA1 Message Date
8f4edd5448 Update flutter_map 2020-08-07 11:09:21 +03:00
10f4c71a49 SemdDataHomeWorker fix 2020-08-07 11:01:00 +03:00
a663838283 Bump version code 2020-07-22 13:33:24 +03:00
3d27f20798 Map fixes 2020-07-22 13:32:34 +03:00
f7d8bf9ae7 Create EntitiesMap widget 2020-07-21 16:29:08 +03:00
9b0b90dba1 Bump version code 2020-07-20 23:43:57 +03:00
5ab34cd32f Fix backround launch of location service 2020-07-20 23:41:13 +03:00
f4b6d7a332 Location requests through foreground service 2020-07-20 16:23:12 +03:00
f87cff7a7e Allow to choose foreground location servcie manualy 2020-07-20 15:21:31 +03:00
d2d037e468 Map card WIP 2020-07-20 13:53:18 +03:00
bf7983d72e Bump build number 2020-07-08 20:22:00 +03:00
40eb564c29 Missed location changes 2020-07-08 20:21:34 +03:00
bc72956365 Remove location priority settings 2020-07-08 18:56:07 +03:00
595406bb7e Resolves #577 request_location_update notification support 2020-07-08 18:30:31 +03:00
01b8ec9b97 Resolves #576 Fix location tracking migration 2020-07-08 18:08:27 +03:00
2126bc4f02 Fix long notification with images conflict 2 2020-07-08 18:01:22 +03:00
d94cdf0d1b Fix long notification with images conflict 2020-07-08 17:59:43 +03:00
3215556440 Resolves #574 2020-07-08 14:22:32 +03:00
a8e7ab6f06 Location notification tweeks 2020-07-08 14:21:00 +03:00
5aa6171c50 Location fixes 2020-07-08 11:49:05 +03:00
2fb296b7a8 Resolves #575 Location settings 2020-07-08 11:12:30 +03:00
e5fe853f0b Fix type cast error in location util 2020-07-08 10:54:41 +03:00
819dfc725d Restart location service after app update 2020-07-07 23:42:21 +03:00
ccbc2eec47 Syncfusion update 2020-07-07 23:11:25 +03:00
dce93966e3 Resolves #571, Resolves #490, Resolves #517 2020-07-07 22:49:51 +03:00
3a7f3db6cd #571 Finish native part 2020-07-07 18:37:44 +03:00
f39dbe3b24 #571 Keep location service after reboot 2020-07-07 00:26:38 +03:00
2ec549af7e WIP #571 2020-07-06 22:41:42 +03:00
7ebaf736b5 Update MDI to 5.3.45 2020-07-06 00:52:12 +03:00
71dcf16d70 WIP #571 2020-07-06 00:38:02 +03:00
6ba1e88b09 WIP #571 2020-07-01 22:59:49 +03:00
08e1327a29 Merge pull request #572 from estevez-dev/release/1.2.0
1200
2020-06-30 13:43:34 +03:00
85693ac5ce 1200 2020-06-30 13:19:30 +03:00
da8efcd43f WIP #571 2020-06-30 13:16:43 +03:00
7ebf5f7c18 WIP #571 Native foreground servise with active location tracking 2020-06-26 12:06:06 +03:00
f84728b948 Update pubspec.yaml 2020-06-23 14:23:32 +03:00
26a62d341e 1.2.0 2020-06-23 13:55:54 +03:00
772bddeb9a Send next alarm updates when network is connected 2020-06-23 13:33:06 +03:00
5b55940ccf Add attributes to next alarm sensor 2020-06-22 23:01:02 +03:00
7683d18e81 Resloves #570 Next Android alarm as sensor 2020-06-22 22:30:51 +03:00
d09afc37b5 Fix in-app purchase 2020-06-13 11:05:06 +03:00
1c686402d0 Update purchase information 2020-06-11 11:47:07 +03:00
5f4a3fbdfc Update device_info to 0.4.2+4 2020-06-03 19:27:26 +00:00
312ed99e9f Remove irrelevant errors log 2020-06-03 18:42:26 +00:00
25e6d51c17 Ignore map state filters in cards 2020-06-03 18:35:06 +00:00
b501574bab Bump version code 2020-06-03 18:25:54 +00:00
53b31d8e90 Fix HTTP exceptions handling for notification images 2020-06-03 18:25:07 +00:00
6d80420a9b Fix cover null attributes handling 2020-06-02 22:07:37 +00:00
e977054139 Fix mobile app registration with unknown manufacturer or model 2020-06-02 21:54:54 +00:00
6367d38524 Fix timer duration parsing 2020-06-02 21:43:32 +00:00
f9b2d7d84c Fix light card with wrong domain entity. Show custom cards if there is entitites 2020-06-02 21:36:45 +00:00
44c28ad106 isExist fix 2020-05-30 10:39:00 +00:00
fec3c525e1 Bump version code 2020-05-30 10:23:21 +00:00
b1bbed6d80 Bump version code 2020-05-30 10:09:48 +00:00
13878cfc51 Fix FCM token crash 2020-05-30 10:02:50 +00:00
be49180205 Update github urls 2020-05-29 18:38:57 +00:00
c4a0b16553 Resolves #567 Login connection timeout issue 2020-05-29 18:26:20 +00:00
caacd5e9f4 Fix display mode name 2020-05-29 18:20:25 +00:00
5fa28abb6c Bump version code 2020-05-29 18:18:38 +00:00
e0a28c0b59 Resolves #563 Fullscreen mode 2020-05-29 18:16:59 +00:00
096e714a04 Real fullscreen for camera view 2020-05-29 16:52:57 +00:00
78893ea01f Fix for cropped screans 2020-05-29 16:35:34 +00:00
90efb29be5 Resolves #555 PayPal donate button removed 2020-05-29 16:25:07 +00:00
fca323c56b Resolves #559 2020-05-29 16:14:39 +00:00
e5fe6af5f3 Resolves #513 Allow to install on SD card 2020-05-29 09:24:23 +00:00
f0090d522d Merge pull request #566 from estevez-dev/rc/1.1.0-b3
Bump version code
2020-05-29 00:16:12 +03:00
edbfd8359b Bump version code 2020-05-28 21:15:26 +00:00
2702bb254a Bump version code 2020-05-28 20:55:08 +00:00
ca7b6ed550 Resolves #564 - Show picture-elements as button if camera_image provided 2020-05-28 20:52:23 +00:00
fb00b5d9ff Replace secure storage with encripted db 2020-05-28 20:23:13 +00:00
7ffba397ce Fix crash whne no google play services available 2020-05-28 19:17:32 +00:00
1080076e3b Merge pull request #562 from estevez-dev/rc/1.1.0-b2
Rc/1.1.0 b2
2020-05-25 18:59:27 +03:00
e295a36465 1151 2020-05-25 15:57:57 +00:00
9a09a83dc6 Request FCM token from native 2020-05-25 15:41:29 +00:00
95ca80949f bump version code 2020-05-25 14:10:29 +00:00
80b5763530 FCM token update and waiting 2020-05-25 14:09:45 +00:00
9a5e35b024 Fix notification event data 2020-05-25 12:34:53 +00:00
4493975676 defer fcm token load 2020-05-25 11:58:14 +00:00
141a68faf7 Update readme 2020-05-25 11:34:55 +00:00
a8efe7dbb6 Remove mobile app integration version check 2020-05-25 11:16:22 +00:00
9608983994 Bump version code 2020-05-25 11:06:08 +00:00
8eb15ab9a4 Notification channel description 2020-05-25 11:05:16 +00:00
aac0cfbb56 Dismiss and auto dismiss for notifications 2020-05-25 10:20:48 +00:00
343494ece0 Fix notification action receiver 2020-05-25 09:54:00 +00:00
b1e5e73278 Fix display name getting issue 2020-05-25 08:52:16 +00:00
9b5a0068fd Fix integer entity names handling in cards 2020-05-25 08:39:30 +00:00
aa26212ddd bump version code 2020-05-25 08:22:52 +00:00
1c45f96706 Remove Firebase hotfix
Firebase messages now handled by native code
2020-05-25 10:39:08 +03:00
c2d5192c51 Filter strange card data 2020-05-25 10:37:02 +03:00
88ae80507c Merge pull request #561 from estevez-dev/actionable_notifications
Actionable notifications
2020-05-25 02:34:18 +03:00
55868d1dfe Resolves #471 Actionable notification 2020-05-24 23:33:20 +00:00
92a1230267 WIP #471 Handling basic notifications in native code 2020-05-24 15:04:55 +00:00
d3f99fb262 WIP #471 Native FCM init 2020-05-24 12:42:31 +00:00
3fdf016c39 Merge pull request #560 from estevez-dev/rc/1.1.0
Rc/1.1.0
2020-05-24 11:26:53 +03:00
8ce0e8aafa remove ndk filters 2020-05-24 08:26:09 +00:00
54f6fb28ef add ndk platform filters 2020-05-24 07:57:00 +00:00
d53825f140 Merge pull request #556 from estevez-dev/rc/1.1.0
Rc/1.1.0
2020-05-23 22:50:18 +03:00
ea7e0f04ce bump build number 2020-05-23 19:27:57 +00:00
cf75989447 trigger 2020-05-23 19:07:53 +00:00
f27d55869b Prevent updates for dev builds 2020-05-23 16:26:42 +00:00
a287f597ad Merge branch 'master' of https://github.com/estevez-dev/ha_client 2020-05-23 15:53:12 +00:00
0698950f3d Changes for CD builds 2020-05-23 15:52:43 +00:00
aa58559ba6 Update README.md 2020-05-23 18:47:59 +03:00
94acc67383 1102 2020-05-23 14:28:43 +00:00
701e6a46df 1.1.0 2020-05-22 16:01:16 +00:00
1ed56ce8f1 Show entity icon on error loading entity picture 2020-05-22 13:31:07 +00:00
a6d1baca77 Fix panel view scroll issue 2020-05-22 12:54:39 +00:00
dbeda6ea68 Default states-like UI if no Lovelace config 2020-05-19 22:01:07 +00:00
3dca28a7da Fix login button display 2020-05-19 21:05:45 +00:00
da4264a409 Quick access buttons 2020-05-18 22:06:45 +00:00
302451e118 Slider labels for light controls 2020-05-18 22:06:45 +00:00
d19dbd389b Update README.md 2020-05-17 17:10:44 +03:00
05ae954b30 Quick actions bar test 2020-05-17 11:16:25 +00:00
840e266381 Update mobile app registration only on full load 2020-05-16 21:25:58 +00:00
01525a2929 Update fcm lib 2020-05-16 21:23:15 +00:00
2eb2596f37 Update push url 2020-05-16 20:04:04 +00:00
699cab3498 Fix reconnect after app resume 2020-05-14 20:53:01 +00:00
353c80b6bc Add path to every view 2020-05-14 16:44:50 +00:00
e4d1a4f823 Resolves #212 2020-05-14 15:38:22 +00:00
78d6b38b92 WIP #212 Light card 2020-05-14 12:13:23 +00:00
1499a91ef7 SHow donate message only after 14 days of use 2020-05-14 11:20:06 +00:00
9b7f7aa380 Fix slider possition change from outside 2020-05-14 10:56:52 +00:00
5683ab5158 WIP: App settings refactoring 2020-05-13 12:46:25 +00:00
a20dfaf05e WIP: AppSettings 2020-05-13 10:57:26 +00:00
61 changed files with 8079 additions and 5459 deletions

View File

@ -1,18 +1,18 @@
# HA Client
## Native Android client for Home Assistant
### With notifications and Lovelace UI support
### With actionable notifications, location tracking and Lovelace UI support
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/store/apps/details?id=com.keyboardcrumbs.haclient)
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)
#### Stable CI 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
#### Last release build status
[![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)
#### Projects used
- [HANotify](https://github.com/Crewski/HANotify) by [Crewski](https://github.com/Crewski)
- [hassalarm](https://github.com/Johboh/hassalarm) by [Johboh](https://github.com/Johboh) distributed under [MIT License](https://github.com/Johboh/hassalarm/blob/master/LICENSE)

View File

@ -79,6 +79,10 @@ flutter {
dependencies {
implementation 'com.google.firebase:firebase-analytics:17.2.2'
implementation 'com.google.firebase:firebase-messaging:20.2.0'
implementation 'com.google.android.gms:play-services-location:17.0.0'
implementation 'androidx.work:work-runtime:2.3.4'
implementation "androidx.concurrent:concurrent-futures:1.0.0"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

View File

@ -1,5 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keyboardcrumbs.hassclient">
package="com.keyboardcrumbs.hassclient"
android:installLocation="auto">
<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />
@ -16,7 +17,6 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="HA Client"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
@ -43,16 +43,45 @@
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name=".MessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<service
android:name=".LocationUpdatesService"
android:enabled="true"
android:exported="false" />
<service
android:name=".LocationRequestService"
android:enabled="true"
android:exported="false" />
<receiver android:name=".NotificationActionReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
</intent-filter>
</receiver>
<receiver android:name=".NextAlarmBroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" />
</intent-filter>
</receiver>
<receiver android:name=".RestartLocationUpdate">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<service
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE"
@ -64,7 +93,7 @@
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
</application>

View File

@ -0,0 +1,146 @@
package com.keyboardcrumbs.hassclient;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.location.Location;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
public class LocationRequestService extends Service {
private static final String TAG = LocationRequestService.class.getSimpleName();
private NotificationManager mNotificationManager;
private LocationRequest mLocationRequest;
private FusedLocationProviderClient mFusedLocationClient;
private LocationCallback mLocationCallback;
private Handler mServiceHandler;
public LocationRequestService() {
}
@Override
public void onCreate() {
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
mLocationCallback = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
super.onLocationResult(locationResult);
onNewLocation(locationResult.getLastLocation());
}
};
mLocationRequest = new LocationRequest();
HandlerThread handlerThread = new HandlerThread(TAG);
handlerThread.start();
mServiceHandler = new Handler(handlerThread.getLooper());
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "Location requests";
NotificationChannel mChannel =
new NotificationChannel(LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
mNotificationManager.createNotificationChannel(mChannel);
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "Service started. startId="+startId);
requestLocationUpdates();
return START_STICKY;
}
@Override
public void onDestroy() {
try {
mFusedLocationClient.removeLocationUpdates(mLocationCallback);
} catch (SecurityException unlikely) {
//When we lost permission
Log.i(TAG, "No location permission");
}
mServiceHandler.removeCallbacksAndMessages(null);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void requestLocationUpdates() {
Log.i(TAG, "Requesting location update in 5 seconds.");
mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
mLocationRequest.setInterval(5000);
startForeground(LocationUtils.ONETIME_NOTIFICATION_ID, LocationUtils.getRequestNotification(this, null, LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID));
try {
mFusedLocationClient.requestLocationUpdates(mLocationRequest,
mLocationCallback, Looper.myLooper());
} catch (SecurityException unlikely) {
stopSelf();
}
}
private void onNewLocation(Location location) {
Log.i(TAG, "New location: " + location);
mNotificationManager.notify(LocationUtils.ONETIME_NOTIFICATION_ID, LocationUtils.getRequestNotification(
this,
location,
LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID
));
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
Data locationData = new Data.Builder()
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
.putDouble("Lat", location.getLatitude())
.putDouble("Long", location.getLongitude())
.putFloat("Acc", location.getAccuracy())
.build();
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
.setConstraints(constraints)
.setInputData(locationData)
.build();
WorkManager
.getInstance(getApplicationContext())
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
stopSelf();
}
}

View File

@ -0,0 +1,154 @@
package com.keyboardcrumbs.hassclient;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.location.Location;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import android.util.Log;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
public class LocationUpdatesService extends Service {
private static final String TAG = LocationUpdatesService.class.getSimpleName();
private NotificationManager mNotificationManager;
private LocationRequest mLocationRequest;
private FusedLocationProviderClient mFusedLocationClient;
private LocationCallback mLocationCallback;
private Handler mServiceHandler;
public LocationUpdatesService() {
}
@Override
public void onCreate() {
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
mLocationCallback = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
super.onLocationResult(locationResult);
onNewLocation(locationResult.getLastLocation());
}
};
mLocationRequest = new LocationRequest();
HandlerThread handlerThread = new HandlerThread(TAG);
handlerThread.start();
mServiceHandler = new Handler(handlerThread.getLooper());
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "Location updates";
NotificationChannel mChannel =
new NotificationChannel(LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
mNotificationManager.createNotificationChannel(mChannel);
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "Service started. startId="+startId);
requestLocationUpdates();
return START_STICKY;
}
@Override
public void onDestroy() {
try {
mFusedLocationClient.removeLocationUpdates(mLocationCallback);
} catch (SecurityException unlikely) {
//When we lost permission
Log.i(TAG, "No location permission");
}
mServiceHandler.removeCallbacksAndMessages(null);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void requestLocationUpdates() {
long requestInterval = LocationUtils.getLocationUpdateIntervals(getApplicationContext());
int priority;
if (requestInterval >= 600000) {
mLocationRequest.setFastestInterval(60000);
priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY;
} else {
priority = LocationRequest.PRIORITY_HIGH_ACCURACY;
}
Log.i(TAG, "Requesting location updates. Every " + requestInterval + "ms with priority of " + priority);
mLocationRequest.setPriority(priority);
mLocationRequest.setInterval(requestInterval);
startForeground(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(this, null, LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID));
try {
mFusedLocationClient.requestLocationUpdates(mLocationRequest,
mLocationCallback, Looper.myLooper());
} catch (SecurityException unlikely) {
stopSelf();
}
}
private void onNewLocation(Location location) {
Log.i(TAG, "New location: " + location);
mNotificationManager.notify(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(
this,
location,
LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID
));
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
Data locationData = new Data.Builder()
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
.putDouble("Lat", location.getLatitude())
.putDouble("Long", location.getLongitude())
.putFloat("Acc", location.getAccuracy())
.build();
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
.setConstraints(constraints)
.setInputData(locationData)
.build();
WorkManager
.getInstance(getApplicationContext())
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
}
}

View File

@ -0,0 +1,112 @@
package com.keyboardcrumbs.hassclient;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.location.Location;
import android.os.Build;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.ListenableWorker;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.WorkerParameters;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.concurrent.TimeUnit;
public class LocationUpdatesWorker extends ListenableWorker {
private Context currentContext;
private LocationCallback callback;
private FusedLocationProviderClient fusedLocationClient;
public LocationUpdatesWorker(Context context, WorkerParameters params) {
super(context, params);
currentContext = context;
}
private void finish() {
fusedLocationClient.removeLocationUpdates(callback);
}
@NonNull
@Override
public ListenableFuture<Result> startWork() {
return CallbackToFutureAdapter.getFuture(completer -> {
fusedLocationClient = LocationServices.getFusedLocationProviderClient(currentContext);
callback = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
super.onLocationResult(locationResult);
Location location = locationResult.getLastLocation();
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
Data locationData = new Data.Builder()
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
.putDouble("Lat", location.getLatitude())
.putDouble("Long", location.getLongitude())
.putFloat("Acc", location.getAccuracy())
.build();
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
.setConstraints(constraints)
.setInputData(locationData)
.build();
WorkManager
.getInstance(getApplicationContext())
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
if (LocationUtils.showNotification(currentContext)) {
NotificationManager notificationManager;
if (android.os.Build.VERSION.SDK_INT >= 23) {
notificationManager = currentContext.getSystemService(NotificationManager.class);
} else {
notificationManager = (NotificationManager)currentContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "Location updates";
NotificationChannel mChannel =
new NotificationChannel(LocationUtils.WORKER_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(mChannel);
}
notificationManager.notify(LocationUtils.WORKER_NOTIFICATION_ID, LocationUtils.getNotification(currentContext, location, LocationUtils.WORKER_NOTIFICATION_CHANNEL_ID));
}
finish();
completer.set(Result.success());
}
};
LocationRequest locationRequest = new LocationRequest();
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
locationRequest.setInterval(5000);
try {
fusedLocationClient.requestLocationUpdates(locationRequest,
callback, Looper.myLooper());
} catch (SecurityException e) {
completer.setException(e);
}
return callback;
});
}
}

View File

@ -0,0 +1,147 @@
package com.keyboardcrumbs.hassclient;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import java.text.DateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
class LocationUtils {
static final String KEY_REQUESTING_LOCATION_UPDATES = "flutter.location-updates-state";
static final String KEY_LOCATION_UPDATE_INTERVAL = "flutter.location-updates-interval";
static final String KEY_LOCATION_SHOW_NOTIFICATION = "flutter.location-updates-show-notification";
static final String WORKER_NOTIFICATION_CHANNEL_ID = "location_worker";
static final int WORKER_NOTIFICATION_ID = 954322;
static final String SERVICE_NOTIFICATION_CHANNEL_ID = "location_service";
static final int SERVICE_NOTIFICATION_ID = 954311;
static final String ONETIME_NOTIFICATION_CHANNEL_ID = "location_request";
static final int ONETIME_NOTIFICATION_ID = 954333;
static final String REQUEST_LOCATION_NOTIFICATION = "request_location_update";
static final String LOCATION_WORK_NAME = "HALocationWorker";
static final String LOCATION_REQUEST_NAME = "HALocationRequest";
static final int LOCATION_UPDATES_DISABLED = 0;
static final int LOCATION_UPDATES_SERVICE = 1;
static final int LOCATION_UPDATES_WORKER = 2;
static final int DEFAULT_LOCATION_UPDATE_INTERVAL_MS = 900000; //15 minutes
static final long MIN_WORKER_LOCATION_UPDATE_INTERVAL_MS = 900000; //15 minutes
static int getLocationUpdatesState(Context context) {
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getInt(KEY_REQUESTING_LOCATION_UPDATES, LOCATION_UPDATES_DISABLED);
}
static long getLocationUpdateIntervals(Context context) {
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getLong(KEY_LOCATION_UPDATE_INTERVAL, DEFAULT_LOCATION_UPDATE_INTERVAL_MS);
}
static boolean showNotification(Context context) {
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getBoolean(KEY_LOCATION_SHOW_NOTIFICATION, true);
}
static void setLocationUpdatesState(Context context, int locationUpdatesState) {
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
.edit()
.putInt(KEY_REQUESTING_LOCATION_UPDATES, locationUpdatesState)
.apply();
}
static void setLocationUpdatesSettings(Context context, long interval, boolean showNotification) {
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
.edit()
.putBoolean(KEY_LOCATION_SHOW_NOTIFICATION, showNotification)
.putLong(KEY_LOCATION_UPDATE_INTERVAL, interval)
.apply();
}
static void startService(Context context) {
Intent myService = new Intent(context, LocationUpdatesService.class);
context.startService(myService);
}
static void startServiceFromBroadcast(Context context) {
Intent serviceIntent = new Intent(context, LocationUpdatesService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent);
} else {
context.startService(serviceIntent);
}
}
static void startWorker(Context context, long interval) {
PeriodicWorkRequest periodicWork = new PeriodicWorkRequest.Builder(LocationUpdatesWorker.class, interval, TimeUnit.MILLISECONDS)
.build();
WorkManager.getInstance(context).enqueueUniquePeriodicWork(LocationUtils.LOCATION_WORK_NAME, ExistingPeriodicWorkPolicy.REPLACE, periodicWork);
}
static void requestLocationOnce(Context context) {
Intent myService = new Intent(context, LocationRequestService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(myService);
} else {
context.startService(myService);
}
//OneTimeWorkRequest oneTimeWork = new OneTimeWorkRequest.Builder(LocationUpdatesWorker.class)
// .build();
//WorkManager.getInstance(context).enqueueUniqueWork(LocationUtils.LOCATION_REQUEST_NAME, ExistingWorkPolicy.REPLACE, oneTimeWork);
}
static Notification getNotification(Context context, Location location, String channelId) {
CharSequence title = "Location tracking";
CharSequence text = location == null ? "Accuracy: unknown" : "Accuracy: " + location.getAccuracy() + " m";
CharSequence bigText = location == null ? "Waiting for location..." : "Time: " + DateFormat.getDateTimeInstance().format(new Date(location.getTime())) +
System.getProperty("line.separator") + "Accuracy: " + location.getAccuracy() + " m" +
System.getProperty("line.separator") + "Location: " + location.getLatitude() + ", " + location.getLongitude();
PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0,
new Intent(context, MainActivity.class), 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
.setContentIntent(activityPendingIntent)
.setContentTitle(title)
.setContentText(text)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(bigText))
.setPriority(-1)
.setOngoing(true)
.setSmallIcon(R.drawable.mini_icon_location)
.setWhen(System.currentTimeMillis());
return builder.build();
}
static Notification getRequestNotification(Context context, Location location, String channelId) {
CharSequence title = "Updating location...";
CharSequence text = location == null ? "Waiting for location..." : "Accuracy: " + location.getAccuracy() + " m";
PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0,
new Intent(context, MainActivity.class), 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
.setContentIntent(activityPendingIntent)
.setContentTitle(title)
.setContentText(text)
.setPriority(-1)
.setOngoing(true)
.setSmallIcon(R.drawable.mini_icon_location)
.setWhen(System.currentTimeMillis());
return builder.build();
}
}

View File

@ -1,15 +1,184 @@
package com.keyboardcrumbs.hassclient;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.work.WorkManager;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant;
import android.Manifest;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.Bundle;
import io.flutter.plugin.common.MethodChannel;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.common.ConnectionResult;
import com.google.firebase.iid.FirebaseInstanceId;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native";
private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34;
private int locationUpdatesType = LocationUtils.LOCATION_UPDATES_DISABLED;
private long locationUpdatesInterval = LocationUtils.DEFAULT_LOCATION_UPDATE_INTERVAL_MS;
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(
(call, result) -> {
Context context = getActivity();
switch (call.method) {
case "getFCMToken":
try {
if (checkPlayServices()) {
FirebaseInstanceId.getInstance().getInstanceId()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
String token = task.getResult().getToken();
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
result.success(token);
} else {
Exception ex = task.getException();
if (ex != null) {
result.error("fcm_error", ex.getMessage(), null);
} else {
result.error("fcm_error", "Unknown", null);
}
}
});
} else {
result.error("google_play_service_error", "Google Play Services unavailable", null);
}
} catch (Exception e) {
result.error("get_token_exception", e.getMessage(), e);
}
break;
case "startLocationService":
try {
locationUpdatesInterval = ((Number)call.argument("location-updates-interval")).longValue();
boolean useForegroundService = (boolean)call.argument("foreground-location-tracking");
if (useForegroundService) {
locationUpdatesType = LocationUtils.LOCATION_UPDATES_SERVICE;
} else {
locationUpdatesType = LocationUtils.LOCATION_UPDATES_WORKER;
}
LocationUtils.setLocationUpdatesSettings(this, locationUpdatesInterval, (boolean)call.argument("location-updates-show-notification"));
if (isNoLocationPermissions()) {
requestLocationPermissions();
} else {
startLocationUpdates();
}
result.success("");
} catch (Exception e) {
result.error("location_error", e.getMessage(), e);
}
break;
case "stopLocationService":
try {
stopLocationUpdates();
result.success("");
} catch (Exception e) {
result.error("location_error", e.getMessage(), e);
}
break;
case "cancelOldLocationWorker":
WorkManager.getInstance(this).cancelAllWorkByTag("haclocation");
result.success("");
break;
}
}
);
}
private boolean checkPlayServices() {
return (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS);
}
private void startLocationUpdates() {
if (locationUpdatesType == LocationUtils.LOCATION_UPDATES_SERVICE) {
LocationUtils.startService(this);
LocationUtils.setLocationUpdatesState(this, locationUpdatesType);
} else if (locationUpdatesType == LocationUtils.LOCATION_UPDATES_WORKER) {
LocationUtils.startWorker(this, locationUpdatesInterval);
LocationUtils.setLocationUpdatesState(this, locationUpdatesType);
} else {
stopLocationUpdates();
}
}
private void stopLocationUpdates() {
Intent myService = new Intent(MainActivity.this, LocationUpdatesService.class);
stopService(myService);
WorkManager.getInstance(this).cancelUniqueWork(LocationUtils.LOCATION_WORK_NAME);
NotificationManager notificationManager;
if (android.os.Build.VERSION.SDK_INT >= 23) {
notificationManager = getSystemService(NotificationManager.class);
} else {
notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
}
notificationManager.cancel(LocationUtils.WORKER_NOTIFICATION_ID);
LocationUtils.setLocationUpdatesState(this, LocationUtils.LOCATION_UPDATES_DISABLED);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onStop() {
super.onStop();
}
private boolean isNoLocationPermissions() {
return PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION);
}
private void requestLocationPermissions() {
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
REQUEST_PERMISSIONS_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startLocationUpdates();
} else {
stopLocationUpdates();
}
}
}
}

View File

@ -0,0 +1,165 @@
package com.keyboardcrumbs.hassclient;
import java.util.Map;
import java.net.URL;
import java.net.URLConnection;
import java.io.InputStream;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import android.webkit.URLUtil;
public class MessagingService extends FirebaseMessagingService {
private static final String TAG = MessagingService.class.getSimpleName();
public static final String NOTIFICATION_ACTION_BROADCAST = "com.keyboardcrumbs.hassclient.haNotificationAction";
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
Map<String, String> data = remoteMessage.getData();
if (data.size() > 0) {
if (data.containsKey("body") || data.containsKey("title")) {
sendNotification(data);
}
}
}
@Override
public void onNewToken(@NonNull String token) {
getApplicationContext().getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
}
private void sendNotification(Map<String, String> data) {
String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription;
boolean autoCancel;
if (!data.containsKey("body")) {
messageBody = "";
} else {
messageBody = data.get("body");
}
if (messageBody != null && messageBody.equals(LocationUtils.REQUEST_LOCATION_NOTIFICATION)) {
Log.d(TAG, "Location update request received");
LocationUtils.requestLocationOnce(this);
return;
}
String customChannelId = data.get("channelId");
if (customChannelId == null) {
channelId = "ha_notify";
channelDescription = "Default notification channel";
} else {
channelId = customChannelId;
channelDescription = channelId;
}
if (!data.containsKey("title")) {
messageTitle = "HA Client";
} else {
messageTitle = data.get("title");
}
if (!data.containsKey("tag")) {
nTag = String.valueOf(System.currentTimeMillis());
} else {
nTag = data.get("tag");
}
if (data.containsKey("dismiss")) {
try {
boolean dismiss = Boolean.parseBoolean(data.get("dismiss"));
if (dismiss) {
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(nTag, 0);
return;
}
} catch (Exception e) {
//nope
}
}
if (data.containsKey("autoDismiss")) {
try {
autoCancel = Boolean.parseBoolean(data.get("autoDismiss"));
} catch (Exception e) {
autoCancel = true;
}
} else {
autoCancel = true;
}
imageUrl = data.get("image");
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
PendingIntent.FLAG_ONE_SHOT);
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.mini_icon)
.setContentTitle(messageTitle)
.setContentText(messageBody)
.setAutoCancel(autoCancel)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent);
Bitmap image = null;
if (URLUtil.isValidUrl(imageUrl)) {
image = getBitmapFromURL(imageUrl);
}
if (image != null) {
notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon)));
notificationBuilder.setLargeIcon(image);
} else {
notificationBuilder.setStyle(new NotificationCompat.BigTextStyle()
.bigText(messageBody));
}
for (int i = 1; i <= 3; i++) {
if (data.containsKey("action" + i)) {
Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class).setAction(NOTIFICATION_ACTION_BROADCAST);
if (autoCancel) {
broadcastIntent.putExtra("tag", nTag);
}
broadcastIntent.putExtra("actionData", data.get("action" + i + "_data"));
PendingIntent actionIntent = PendingIntent.getBroadcast(this, i, broadcastIntent, PendingIntent.FLAG_CANCEL_CURRENT);
notificationBuilder.addAction(R.drawable.blank_icon, data.get("action" + i), actionIntent);
}
}
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Since android Oreo notification channel is needed.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(channelId,
channelDescription,
NotificationManager.IMPORTANCE_HIGH);
notificationManager.createNotificationChannel(channel);
}
notificationManager.notify(nTag, 0 /* ID of notification */, notificationBuilder.build());
}
private Bitmap getBitmapFromURL(String imageUrl) {
try {
URL url = new URL(imageUrl);
URLConnection connection = url.openConnection();
connection.setDoInput(true);
connection.connect();
InputStream input = connection.getInputStream();
return BitmapFactory.decodeStream(input);
} catch (Exception e) {
return null;
}
}
}

View File

@ -0,0 +1,54 @@
package com.keyboardcrumbs.hassclient;
import android.app.AlarmManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import java.util.concurrent.TimeUnit;
public class NextAlarmBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) {
return;
}
final boolean isBootIntent = Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction());
final boolean isNextAlarmIntent = AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equalsIgnoreCase(intent.getAction());
if (!isBootIntent && !isNextAlarmIntent) {
return;
}
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
Data workerData = new Data.Builder()
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_NEXT_ALARM)
.build();
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10,
TimeUnit.SECONDS)
.setInputData(workerData)
.setConstraints(constraints)
.build();
WorkManager
.getInstance(context)
.enqueueUniqueWork("NextAlarmUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
}
}

View File

@ -0,0 +1,58 @@
package com.keyboardcrumbs.hassclient;
import android.content.Context;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.app.NotificationManager;
import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import java.util.concurrent.TimeUnit;
public class NotificationActionReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) {
return;
}
String intentAction = intent.getAction();
if (intentAction == null || !intentAction.equalsIgnoreCase(MessagingService.NOTIFICATION_ACTION_BROADCAST)) {
return;
}
String rawActionData = intent.getStringExtra("actionData");
if (intent.hasExtra("tag")) {
String notificationTag = intent.getStringExtra("tag");
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationTag, 0);
}
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
Data workerData = new Data.Builder()
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_NOTIFICATION_ACTION)
.putString("rawActionData", rawActionData)
.build();
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10,
TimeUnit.SECONDS)
.setInputData(workerData)
.setConstraints(constraints)
.build();
WorkManager
.getInstance(context)
.enqueueUniqueWork("NotificationAction", ExistingWorkPolicy.APPEND, uploadWorkRequest);
}
}

View File

@ -0,0 +1,15 @@
package com.keyboardcrumbs.hassclient;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class RestartLocationUpdate extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
if (LocationUtils.getLocationUpdatesState(context) == LocationUtils.LOCATION_UPDATES_SERVICE &&
(Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction()) || Intent.ACTION_MY_PACKAGE_REPLACED.equalsIgnoreCase(intent.getAction()))) {
LocationUtils.startServiceFromBroadcast(context);
}
}
}

View File

@ -0,0 +1,220 @@
package com.keyboardcrumbs.hassclient;
import android.app.AlarmManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.BatteryManager;
import android.util.Log;
import android.webkit.URLUtil;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
public class SendDataHomeWorker extends Worker {
public static final String DATA_TYPE_KEY = "dataType";
public static final int DATA_TYPE_LOCATION = 1;
public static final int DATA_TYPE_NEXT_ALARM = 2;
public static final int DATA_TYPE_NOTIFICATION_ACTION = 3;
private Context currentContext;
private static final String TAG = "SendDataHomeWorker";
public static final String KEY_LAT_ARG = "Lat";
public static final String KEY_LONG_ARG = "Long";
public static final String KEY_ACC_ARG = "Acc";
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:00", Locale.ENGLISH);
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:00", Locale.ENGLISH);
public SendDataHomeWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
currentContext = context;
}
@NonNull
@Override
public Result doWork() {
Log.d(TAG, "Start sending data home");
SharedPreferences prefs = currentContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
String webhookId = prefs.getString("flutter.app-webhook-id", null);
if (webhookId != null) {
try {
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
"://" +
prefs.getString("flutter.hassio-domain", "") +
":" +
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
if (URLUtil.isValidUrl(requestUrl)) {
int dataType = getInputData().getInt(DATA_TYPE_KEY, 0);
String stringRequest;
if (dataType == DATA_TYPE_LOCATION) {
Log.d(TAG, "Location data");
stringRequest = getLocationDataToSend();
} else if (dataType == DATA_TYPE_NEXT_ALARM) {
Log.d(TAG, "Next alarm data");
stringRequest = getNextAlarmDataToSend();
} else if (dataType == DATA_TYPE_NOTIFICATION_ACTION) {
Log.d(TAG, "Notification action data");
stringRequest = getNotificationActionData();
} else {
Log.e(TAG, "doWork() unknown data type: " + dataType);
return Result.failure();
}
try {
URL url = new URL(requestUrl);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setDoOutput(true);
assert stringRequest != null;
byte[] outputBytes = stringRequest.getBytes(StandardCharsets.UTF_8);
OutputStream os = urlConnection.getOutputStream();
os.write(outputBytes);
int responseCode = urlConnection.getResponseCode();
urlConnection.disconnect();
if (responseCode >= 300) {
return Result.retry();
}
} catch (Exception e) {
Log.e(TAG, "Error sending data", e);
return Result.retry();
}
} else {
Log.w(TAG, "Invalid HA url");
return Result.failure();
}
} catch (Exception e) {
Log.e(TAG, "Error =(", e);
return Result.failure();
}
} else {
Log.w(TAG, "Webhook id not found");
return Result.failure();
}
return Result.success();
}
private String getLocationDataToSend() {
try {
JSONObject dataToSend = new JSONObject();
dataToSend.put("type", "update_location");
JSONObject dataObject = new JSONObject();
JSONArray gps = new JSONArray();
gps.put(0, getInputData().getDouble(KEY_LAT_ARG, 0));
gps.put(1, getInputData().getDouble(KEY_LONG_ARG, 0));
dataObject.put("gps", gps);
dataObject.put("gps_accuracy", getInputData().getFloat(KEY_ACC_ARG, 0));
BatteryManager bm;
if (android.os.Build.VERSION.SDK_INT >= 23) {
bm = currentContext.getSystemService(BatteryManager.class);
} else {
bm = (BatteryManager)currentContext.getSystemService(Context.BATTERY_SERVICE);
}
int batLevel = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
dataObject.put("battery", batLevel);
dataToSend.put("data", dataObject);
return dataToSend.toString();
} catch (Exception e) {
Log.e(TAG,"getLocationDataToSend", e);
return null;
}
}
private String getNotificationActionData() {
try {
String rawActionData = getInputData().getString("rawActionData");
if (rawActionData == null || rawActionData.length() == 0) {
Log.e(TAG,"getNotificationActionData rawAction data is empty");
return null;
}
JSONObject actionData = new JSONObject(rawActionData);
JSONObject dataToSend = new JSONObject();
JSONObject requestData = new JSONObject();
if (actionData.getString("action").equals("call-service")) {
dataToSend.put("type", "call_service");
requestData.put("domain", actionData.getString("service").split("\\.")[0]);
requestData.put("service", actionData.getString("service").split("\\.")[1]);
if (actionData.has("service_data")) {
requestData.put("service_data", actionData.get("service_data"));
}
} else {
dataToSend.put("type", "fire_event");
requestData.put("event_type", "ha_client_event");
JSONObject eventData = new JSONObject();
eventData.put("action", actionData.getString("action"));
requestData.put("event_data", eventData);
}
dataToSend.put("data", requestData);
return dataToSend.toString();
} catch (Exception e) {
Log.e(TAG,"getNotificationActionData", e);
return null;
}
}
private String getNextAlarmDataToSend() {
try {
final AlarmManager alarmManager;
if (android.os.Build.VERSION.SDK_INT >= 23) {
alarmManager = currentContext.getSystemService(AlarmManager.class);
} else {
alarmManager = (AlarmManager)currentContext.getSystemService(Context.ALARM_SERVICE);
}
final AlarmManager.AlarmClockInfo alarmClockInfo = alarmManager.getNextAlarmClock();
JSONObject dataToSend = new JSONObject();
dataToSend.put("type", "update_sensor_states");
JSONArray dataArray = new JSONArray();
JSONObject sensorData = new JSONObject();
JSONObject sensorAttrs = new JSONObject();
sensorData.put("unique_id", "next_alarm");
sensorData.put("type", "sensor");
final long triggerTimestamp;
if (alarmClockInfo != null) {
triggerTimestamp = alarmClockInfo.getTriggerTime();
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(triggerTimestamp);
Date date = calendar.getTime();
sensorData.put("state", DATE_TIME_FORMAT.format(date));
sensorAttrs.put("date", DATE_FORMAT.format(date));
sensorAttrs.put("time", TIME_FORMAT.format(date));
sensorAttrs.put("timestamp", triggerTimestamp);
} else {
sensorData.put("state", "");
sensorAttrs.put("date", "");
sensorAttrs.put("time", "");
sensorAttrs.put("timestamp", 0);
}
sensorData.put("icon", "mdi:alarm");
sensorData.put("attributes", sensorAttrs);
dataArray.put(0, sensorData);
dataToSend.put("data", dataArray);
return dataToSend.toString();
} catch (Exception e) {
Log.e(TAG,"getNextAlarmDataToSend", e);
return null;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

View File

@ -1,6 +1,4 @@
org.gradle.jvmargs=-Xmx2g
org.gradle.daemon=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx512m
android.useAndroidX=true
android.enableJetifier=true
android.enableR8=true

Binary file not shown.

View File

@ -10,7 +10,7 @@ class Badges extends StatelessWidget {
List<EntityWrapper> entitiesToShow = badges.getEntitiesToShow();
if (entitiesToShow.isNotEmpty) {
if (ConnectionManager().scrollBadges) {
if (AppSettings().scrollBadges) {
return ConstrainedBox(
constraints: BoxConstraints.tightFor(height: 112),
child: SingleChildScrollView(

View File

@ -21,7 +21,6 @@ class CardData {
switch (rawData['type']) {
case CardType.ENTITIES:
case CardType.HISTORY_GRAPH:
case CardType.MAP:
case CardType.PICTURE_GLANCE:
case CardType.SENSOR:
case CardType.ENTITY:
@ -35,8 +34,22 @@ class CardData {
case CardType.ALARM_PANEL:
return AlarmPanelCardData(rawData);
break;
case CardType.ENTITY_BUTTON:
case CardType.LIGHT:
return LightCardData(rawData);
break;
case CardType.PICTURE_ELEMENTS:
//TODO temporary solution
if (rawData.containsKey('camera_image')) {
rawData['entity'] = rawData['camera_image'];
return ButtonCardData(rawData);
} else {
return CardData(null);
}
break;
case CardType.MAP:
return MapCardData(rawData);
break;
case CardType.ENTITY_BUTTON:
case CardType.BUTTON:
case CardType.PICTURE_ENTITY:
return ButtonCardData(rawData);
@ -79,6 +92,12 @@ class CardData {
return BadgesData(rawData);
break;
default:
if (rawData.containsKey('entity')) {
rawData['entities'] = [rawData['entity']];
}
if (rawData.containsKey('entities') && rawData['entities'] is List) {
return EntitiesCardData(rawData);
}
return CardData(null);
}
} catch (error, stacktrace) {
@ -92,7 +111,11 @@ class CardData {
type = rawData['type'];
conditions = rawData['conditions'] ?? [];
showEmpty = rawData['show_empty'] ?? true;
stateFilter = rawData['state_filter'] ?? [];
if (rawData.containsKey('state_filter') && rawData['state_filter'] is List) {
stateFilter = rawData['state_filter'];
} else {
stateFilter = [];
}
} else {
type = CardType.UNKNOWN;
conditions = [];
@ -195,7 +218,7 @@ class BadgesData extends CardData {
entities.add(
EntityWrapper(
entity: HomeAssistant().entities.get(rawBadge['entity']),
overrideName: rawBadge["name"],
overrideName: rawBadge["name"]?.toString(),
overrideIcon: rawBadge["icon"],
)
);
@ -221,7 +244,7 @@ class BadgesData extends CardData {
entities.add(
EntityWrapper(
entity: e,
overrideName: rawEntity["name"],
overrideName: rawEntity["name"]?.toString(),
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []),
uiAction: EntityUIAction(rawEntityData: rawEntity)
@ -246,7 +269,7 @@ class EntitiesCardData extends CardData {
EntitiesCardData(rawData) : super(rawData) {
//Parsing card data
title = rawData['title'];
title = rawData['title']?.toString();
icon = rawData['icon'] is String ? rawData['icon'] : null;
stateColor = rawData['state_color'] ?? false;
showHeaderToggle = rawData['show_header_toggle'] ?? false;
@ -277,7 +300,7 @@ class EntitiesCardData extends CardData {
EntityWrapper(
entity: Entity.callService(
icon: rawEntity["icon"],
name: rawEntity["name"],
name: rawEntity["name"]?.toString(),
service: rawEntity["service"],
actionName: rawEntity["action_name"]
),
@ -296,7 +319,7 @@ class EntitiesCardData extends CardData {
entities.add(EntityWrapper(
entity: Entity.weblink(
icon: rawEntity["icon"],
name: rawEntity["name"],
name: rawEntity["name"]?.toString(),
url: rawEntity["url"]
),
stateColor: rawEntity["state_color"] ?? stateColor,
@ -309,7 +332,7 @@ class EntitiesCardData extends CardData {
EntityWrapper(
entity: e,
stateColor: rawEntity["state_color"] ?? stateColor,
overrideName: rawEntity["name"],
overrideName: rawEntity["name"]?.toString(),
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)
@ -336,7 +359,7 @@ class AlarmPanelCardData extends CardData {
AlarmPanelCardData(rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
name = rawData['name']?.toString();
states = rawData['states'];
//Parsing entity
var entitiId = rawData["entity"];
@ -356,6 +379,45 @@ class AlarmPanelCardData extends CardData {
}
class LightCardData extends CardData {
String name;
String icon;
@override
Widget buildCardWidget() {
if (this.entity != null && this.entity.entity is LightEntity) {
return LightCard(card: this);
}
return ErrorCard(
errorText: 'Specify an entity from within the light domain.',
showReportButton: false,
);
}
LightCardData(rawData) : super(rawData) {
//Parsing card data
name = rawData['name']?.toString();
icon = rawData['icon'] is String ? rawData['icon'] : null;
//Parsing entity
var entitiId = rawData["entity"];
if (entitiId != null && entitiId is String) {
if (HomeAssistant().entities.isExist(entitiId)) {
entities.add(EntityWrapper(
entity: HomeAssistant().entities.get(entitiId),
overrideName: name,
overrideIcon: icon,
uiAction: EntityUIAction()..tapAction = EntityUIAction.toggle
));
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
} else {
entities.add(EntityWrapper(entity: Entity.missed('$entitiId')));
}
}
}
class ButtonCardData extends CardData {
String name;
@ -372,7 +434,7 @@ class ButtonCardData extends CardData {
ButtonCardData(rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
name = rawData['name']?.toString();
icon = rawData['icon'] is String ? rawData['icon'] : null;
showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true;
@ -435,7 +497,7 @@ class GaugeCardData extends CardData {
GaugeCardData(rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
name = rawData['name']?.toString();
unit = rawData['unit'];
if (rawData['min'] is int) {
min = rawData['min'].toDouble();
@ -487,7 +549,7 @@ class GlanceCardData extends CardData {
GlanceCardData(rawData) : super(rawData) {
//Parsing card data
title = rawData["title"];
title = rawData["title"]?.toString();
showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true;
showState = rawData['show_state'] ?? true;
@ -509,7 +571,7 @@ class GlanceCardData extends CardData {
EntityWrapper(
entity: e,
stateColor: stateColor,
overrideName: rawEntity["name"],
overrideName: rawEntity["name"]?.toString(),
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)
@ -584,6 +646,52 @@ class MarkdownCardData extends CardData {
}
class MapCardData extends CardData {
String title;
@override
Widget buildCardWidget() {
return MapCard(card: this);
}
MapCardData(rawData) : super(rawData) {
//Parsing card data
title = rawData['title'];
List<dynamic> geoLocationSources = rawData['geo_location_sources'] ?? [];
if (geoLocationSources.isNotEmpty) {
//TODO add entities by source
}
var rawEntities = rawData["entities"] ?? [];
rawEntities.forEach((rawEntity) {
if (rawEntity is String) {
if (HomeAssistant().entities.isExist(rawEntity)) {
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
}
} else {
if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
entities.add(
EntityWrapper(
entity: e,
stateColor: stateColor,
overrideName: rawEntity["name"]?.toString(),
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
}
}
});
}
}
class MediaControlCardData extends CardData {
@override

View File

@ -2,12 +2,21 @@ part of '../main.dart';
class ErrorCard extends StatelessWidget {
final ErrorCardData card;
final String errorText;
final bool showReportButton;
const ErrorCard({Key key, this.card}) : super(key: key);
const ErrorCard({Key key, this.card, this.errorText, this.showReportButton: true}) : super(key: key);
@override
Widget build(BuildContext context) {
String error;
if (errorText == null) {
error = 'There was an error showing ${card?.type}';
} else {
error = errorText;
}
return CardWrapper(
color: Theme.of(context).errorColor,
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
@ -15,21 +24,25 @@ class ErrorCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
'There was an error rendering card: ${card.type}. Please copy card config to clipboard and report this issue. Thanks!',
error,
textAlign: TextAlign.center,
),
card != null ?
RaisedButton(
onPressed: () {
Clipboard.setData(new ClipboardData(text: card.cardConfig));
},
child: Text('Copy card config'),
),
) :
Container(width: 0, height: 0),
showReportButton ?
RaisedButton(
onPressed: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
},
child: Text('Report issue'),
)
) :
Container(width: 0, height: 0)
],
),
)

161
lib/cards/light_card.dart Normal file
View File

@ -0,0 +1,161 @@
part of '../main.dart';
class LightCard extends StatefulWidget {
final LightCardData card;
LightCard({Key key, this.card}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _LightCardState();
}
}
class _LightCardState extends State<LightCard> {
double _actualBrightness;
double _newBrightness;
bool _changedHere = false;
@override
void initState() {
super.initState();
}
void _setBrightness(double value, LightEntity entity) {
setState((){
_newBrightness = value;
_changedHere = true;
});
ConnectionManager().callService(
domain: entity.domain,
service: "turn_on",
entityId: entity.entityId,
data: {"brightness": value.round()}
);
}
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = widget.card.entity;
LightEntity entity = entityWrapper.entity;
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
return EntityModel(
entityWrapper: widget.card.entity,
child: MissedEntityWidget(),
handleTap: false,
);
}
entityWrapper.overrideName = widget.card.name ??
entityWrapper.displayName;
entityWrapper.overrideIcon = widget.card.icon ??
entityWrapper.icon;
if (!_changedHere) {
_actualBrightness = (entity.brightness ?? 0).toDouble();
_newBrightness = _actualBrightness;
} else {
_changedHere = false;
}
Color lightColor = entity.color?.toColor();
Color color;
if (lightColor != null && lightColor != Colors.white) {
color = lightColor;
} else {
color = Theme.of(context).accentColor;
}
return CardWrapper(
padding: EdgeInsets.all(4),
child: EntityModel(
entityWrapper: entityWrapper,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
ConstrainedBox(
constraints: BoxConstraints.loose(Size(200, 200)),
child: AspectRatio(
aspectRatio: 1,
child: Stack(
alignment: Alignment.center,
children: <Widget>[
SfRadialGauge(
axes: <RadialAxis>[
RadialAxis(
onAxisTapped: (val) {
_setBrightness(val, entity);
},
maximum: 255,
minimum: 0,
showLabels: false,
showTicks: false,
axisLineStyle: AxisLineStyle(
thickness: 0.05,
thicknessUnit: GaugeSizeUnit.factor,
color: HAClientTheme().getDisabledStateColor(context)
),
pointers: <GaugePointer>[
RangePointer(
value: _actualBrightness,
sizeUnit: GaugeSizeUnit.factor,
width: 0.05,
color: color,
enableAnimation: true,
animationType: AnimationType.bounceOut,
),
MarkerPointer(
value: _newBrightness,
markerType: MarkerType.circle,
markerHeight: 20,
markerWidth: 20,
enableDragging: true,
onValueChangeEnd: (val) {
_setBrightness(val, entity);
},
color: HAClientTheme().getColorByEntityState(entity.state, context)
//enableAnimation: true,
//animationType: AnimationType.bounceOut,
)
]
)
],
),
FractionallySizedBox(
heightFactor: 0.4,
widthFactor: 0.4,
child: AspectRatio(
aspectRatio: 1,
child: InkResponse(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
child: FittedBox(
fit: BoxFit.contain,
child: EntityIcon(
showBadge: false,
padding: EdgeInsets.all(0)
)
)
)
)
)
],
)
)
),
EntityName(
padding: EdgeInsets.all(0),
wordsWrap: true,
maxLines: 3,
textOverflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
)
],
),
handleTap: true
)
);
}
}

88
lib/cards/map_card.dart Normal file
View File

@ -0,0 +1,88 @@
part of '../main.dart';
class MapCard extends StatefulWidget {
final MapCardData card;
const MapCard({Key key, this.card}) : super(key: key);
@override
_MapCardState createState() => _MapCardState();
}
class _MapCardState extends State<MapCard> {
void _openMap(BuildContext context) {
Navigator.of(context).push(MaterialPageRoute(
builder: (bc) {
return Scaffold(
primary: false,
/*appBar: new AppBar(
backgroundColor: Colors.transparent,
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
actions: <Widget>[
IconButton(
icon: Icon(Icons.fullscreen),
onPressed: () {},
)
],
// 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.card.title ?? ""}"),
),*/
body: Container(
color: Theme.of(context).primaryColor,
child: SafeArea(
child: Stack(
children: <Widget>[
EntitiesMap(
entities: widget.card.entities,
interactive: true
),
Positioned(
top: 0,
left: 0,
child: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
})
)
],
)
)
)
);
}
));
}
@override
Widget build(BuildContext context) {
return CardWrapper(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
CardHeader(name: widget.card.title),
Stack(
children: <Widget>[
GestureDetector(
onTap: () => _openMap(context),
child: EntitiesMap(
aspectRatio: 1,
interactive: false,
entities: widget.card.entities,
)
),
Positioned(
bottom: 0,
left: 0,
child: Text('Tap to open interactive map', style: Theme.of(context).textTheme.caption)
)
],
),
],
)
);
}
}

View File

@ -7,6 +7,6 @@ class UnsupportedCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
return Container(height: 20);
}
}

View File

@ -4,12 +4,14 @@ class CardWrapper extends StatelessWidget {
final Widget child;
final EdgeInsets padding;
final Color color;
const CardWrapper({Key key, this.child, this.padding: const EdgeInsets.all(0)}) : super(key: key);
const CardWrapper({Key key, this.child, this.color, this.padding: const EdgeInsets.all(0)}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
color: color,
child: Padding(
padding: padding,
child: child

View File

@ -0,0 +1,73 @@
part of '../../main.dart';
class EntitiesMap extends StatelessWidget {
final List<EntityWrapper> entities;
final bool interactive;
final double aspectRatio;
final LatLng center;
final double zoom;
const EntitiesMap({Key key, this.entities: const [], this.aspectRatio, this.interactive: true, this.center, this.zoom}) : super(key: key);
@override
Widget build(BuildContext context) {
List<Marker> markers = [];
List<LatLng> points = [];
entities.forEach((entityWrapper) {
double lat = entityWrapper.entity._getDoubleAttributeValue("latitude");
double long = entityWrapper.entity._getDoubleAttributeValue("longitude");
if (lat != null && long != null) {
points.add(LatLng(lat, long));
markers.add(
Marker(
width: 36,
height: 36,
point: LatLng(lat, long),
builder: (ctx) => EntityModel(
handleTap: true,
entityWrapper: entityWrapper,
child: EntityIcon(
size: 36,
),
)
)
);
}
});
MapOptions mapOptions;
if (center != null) {
mapOptions = MapOptions(
interactive: interactive,
center: center,
zoom: zoom ?? 10,
);
} else {
mapOptions = MapOptions(
interactive: interactive,
bounds: LatLngBounds.fromPoints(points),
boundsOptions: FitBoundsOptions(padding: EdgeInsets.all(40)),
);
}
Widget map = FlutterMap(
options: mapOptions,
layers: [
new TileLayerOptions(
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: ['a', 'b', 'c']
),
new MarkerLayerOptions(
markers: markers,
),
],
);
if (aspectRatio != null) {
return AspectRatio(
aspectRatio: aspectRatio,
child: map
);
}
return map;
}
}

View File

@ -42,7 +42,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
_webViewHtml = Uri.dataFromString(
file.replaceFirst('{{stream_url}}', '${ConnectionManager().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
file.replaceFirst('{{stream_url}}', '${AppSettings().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
mimeType: 'text/html',
encoding: Encoding.getByName('utf-8')
).toString();
@ -69,7 +69,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
}
Future _loadMJPEG() async {
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
_streamUrl = '${AppSettings().httpWebHost}/api/camera_proxy_stream/${_entity
.entityId}?token=${_entity.attributes['access_token']}';
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
var file = await rootBundle.loadString('assets/html/cameraView.html');

View File

@ -40,8 +40,8 @@ class CoverEntity extends Entity {
CoverEntity.SUPPORT_SET_TILT_POSITION);
double get currentPosition => _getDoubleAttributeValue('current_position');
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position');
double get currentPosition => _getDoubleAttributeValue('current_position') ?? 0;
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position') ?? 0;
bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0);
bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed));
bool get canTiltBeOpened => currentTiltPosition < 100;

View File

@ -78,9 +78,21 @@ class Entity {
chartType: EntityHistoryWidgetType.simple
);
String get displayName =>
attributes["friendly_name"] ??
(attributes["name"] ?? (entityId != null && entityId.contains('.')) ? entityId.split(".")[1].replaceAll("_", " ") : "");
String get displayName {
if (attributes.containsKey('friendly_name')) {
return attributes['friendly_name'];
}
if (attributes.containsKey('name')) {
return attributes['name'];
}
if (entityId == null) {
return "";
}
if (entityId.contains(".")) {
return entityId.split(".")[1].replaceAll("_", " ");
}
return entityId;
}
bool get isView =>
(domain == "group") &&

View File

@ -7,8 +7,9 @@ class EntityIcon extends StatelessWidget {
final EdgeInsetsGeometry imagePadding;
final double size;
final Color color;
final bool showBadge;
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0), this.iconPadding, this.imagePadding}) : super(key: key);
const EntityIcon({Key key, this.color, this.showBadge: true, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0), this.iconPadding, this.imagePadding}) : super(key: key);
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
if (entityId == null) {
@ -48,70 +49,23 @@ class EntityIcon extends StatelessWidget {
);
} else {
if (entityWrapper.entityPicture != null) {
iconWidget = Container(
height: size+12,
width: size+12,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit:BoxFit.cover,
image: CachedNetworkImageProvider(
"${entityWrapper.entityPicture}"
),
)
iconWidget = ClipOval(
child: CachedNetworkImage(
imageUrl: '${entityWrapper.entityPicture}',
width: size+12,
fit: BoxFit.cover,
height: size+12,
errorWidget: (context, str, dyn) {
return Padding(
padding: iconPadding ?? padding,
child: _buildIcon(entityWrapper, iconColor)
);
},
),
);
isPicture = true;
} else {
String iconName = entityWrapper.icon;
int iconCode = 0;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId,
entityWrapper.entity.deviceClass, entityWrapper.entity.state); //
}
if (entityWrapper.entity is LightEntity &&
(entityWrapper.entity as LightEntity).supportColor &&
(entityWrapper.entity as LightEntity).color != null &&
(entityWrapper.entity as LightEntity).color.toColor() != Colors.white
) {
Color lightColor = (entityWrapper.entity as LightEntity).color.toColor();
iconWidget = Stack(
children: <Widget>[
Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: iconColor,
),
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)
)
]
),
),
)
],
);
} else {
iconWidget = Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: iconColor,
);
}
iconWidget = _buildIcon(entityWrapper, iconColor);
}
}
EdgeInsetsGeometry computedPadding;
@ -127,4 +81,58 @@ class EntityIcon extends StatelessWidget {
child: iconWidget,
);
}
Widget _buildIcon(EntityWrapper entityWrapper, Color iconColor) {
Widget iconWidget;
String iconName = entityWrapper.icon;
int iconCode = 0;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId,
entityWrapper.entity.deviceClass, entityWrapper.entity.state); //
}
if (showBadge && entityWrapper.entity is LightEntity &&
(entityWrapper.entity as LightEntity).supportColor &&
(entityWrapper.entity as LightEntity).color != null &&
(entityWrapper.entity as LightEntity).color.toColor() != Colors.white
) {
Color lightColor = (entityWrapper.entity as LightEntity).color.toColor();
iconWidget = Stack(
children: <Widget>[
Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: iconColor,
),
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)
)
]
),
),
)
],
);
} else {
iconWidget = Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: iconColor,
);
}
return iconWidget;
}
}

View File

@ -3,7 +3,7 @@ part of '../main.dart';
class EntityWrapper {
String overrideName;
final String overrideIcon;
String overrideIcon;
final bool stateColor;
EntityUIAction uiAction;
Entity entity;
@ -61,7 +61,7 @@ class EntityWrapper {
case EntityUIAction.navigate: {
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
//TODO handle local urls
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.tapService}');
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.tapService}');
} else {
Launcher.launchURLInBrowser(uiAction.tapService);
}
@ -101,7 +101,7 @@ class EntityWrapper {
case EntityUIAction.navigate: {
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
//TODO handle local urls
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.holdService}');
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.holdService}');
} else {
Launcher.launchURLInBrowser(uiAction.holdService);
}
@ -141,7 +141,7 @@ class EntityWrapper {
case EntityUIAction.navigate: {
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
//TODO handle local urls
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.doubleTapService}');
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.doubleTapService}');
} else {
Launcher.launchURLInBrowser(uiAction.doubleTapService);
}

View File

@ -134,8 +134,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpBrightness = value.round();
});
},
min: 1.0,
max: 255.0,
min: 1,
max: 255,
divisions: 254,
label: '${val?.toInt() ?? ''}',
onChangeEnd: (value) => _setBrightness(entity, value),
value: val,
leading: Icon(Icons.brightness_5),
@ -155,10 +157,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpWhiteValue = value.round();
});
},
min: 0.0,
max: 255.0,
min: 0,
max: 255,
divisions: 255,
label: '$_tmpWhiteValue',
onChangeEnd: (value) => _setWhiteValue(entity, value),
value: _tmpWhiteValue == null ? 0.0 : _tmpWhiteValue.toDouble(),
value: _tmpWhiteValue?.toDouble() ?? 0.0,
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")),
title: "White",
);
@ -188,6 +192,8 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
onChangeEnd: (value) => _setColorTemp(entity, value),
max: entity.maxMireds,
min: entity.minMireds,
divisions: (entity.maxMireds - entity.minMireds).toInt(),
label: '$_tmpColorTemp',
onChanged: (value) {
setState(() {
_changedHere = true;

View File

@ -8,8 +8,8 @@ class TimerEntity extends Entity {
@override
void update(Map rawData, String webHost) {
super.update(rawData, webHost);
String durationSource = "${attributes["duration"]}";
if (durationSource != null && durationSource.isNotEmpty) {
if (attributes.containsKey('duration')) {
String durationSource = "${attributes["duration"]}";
try {
List<String> durationList = durationSource.split(":");
if (durationList.length == 1) {

View File

@ -13,9 +13,10 @@ class UniversalSlider extends StatefulWidget {
final double max;
final double value;
final int divisions;
final String label;
final EdgeInsets padding;
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);
const UniversalSlider({Key key, this.onChanged, this.label, 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
State<StatefulWidget> createState() {
@ -28,10 +29,10 @@ class UniversalSliderState extends State<UniversalSlider> {
double _value;
bool _changeStarted = false;
bool _changedHere = false;
@override
void initState() {
_value = widget.value;
super.initState();
}
@ -39,6 +40,11 @@ class UniversalSliderState extends State<UniversalSlider> {
Widget build(BuildContext context) {
List <Widget> row = [];
List <Widget> col = [];
if (!_changedHere) {
_value = widget.value;
} else {
_changedHere = false;
}
if (widget.leading != null) {
row.add(widget.leading);
}
@ -49,6 +55,7 @@ class UniversalSliderState extends State<UniversalSlider> {
min: widget.min ?? 0,
max: widget.max ?? 100,
activeColor: widget.activeColor,
label: widget.label,
onChangeStart: (value) {
_changeStarted = true;
widget.onChangeStart?.call(value);
@ -57,6 +64,7 @@ class UniversalSliderState extends State<UniversalSlider> {
onChanged: (value) {
setState(() {
_value = value;
_changedHere = true;
});
widget.onChanged?.call(value);
},
@ -64,6 +72,7 @@ class UniversalSliderState extends State<UniversalSlider> {
_changeStarted = false;
setState(() {
_value = value;
_changedHere = true;
});
Timer(Duration(milliseconds: 500), () {
if (!_changeStarted) {

View File

@ -149,7 +149,7 @@ class EntityCollection {
}
bool isExist(String entityId) {
return _allEntities[entityId] != null;
return _allEntities.containsKey(entityId);
}
List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) {

View File

@ -14,7 +14,7 @@ class HomeAssistant {
HomeAssistantUI ui;
Map _instanceConfig = {};
String _userName;
String _lovelaceDashbordUrl;
String currentDashboardPath;
HSVColor savedColor;
int savedPlayerPosition;
String sendToPlayerId;
@ -22,15 +22,11 @@ class HomeAssistant {
Map services;
bool autoUi = false;
String fcmToken;
Map _rawLovelaceData;
var _rawStates;
var _rawUserInfo;
var _rawPanels;
set lovelaceDashboardUrl(String val) => _lovelaceDashbordUrl = val;
List<Panel> panels = [];
Duration fetchTimeout = Duration(seconds: 30);
@ -63,7 +59,7 @@ class HomeAssistant {
_fetchCompleter = Completer();
List<Future> futures = [];
if (!uiOnly) {
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
if (entities == null) entities = EntityCollection(AppSettings().httpWebHost);
futures.add(_getStates(null));
futures.add(_getConfig(null));
futures.add(_getUserInfo(null));
@ -77,7 +73,6 @@ class HomeAssistant {
if (isComponentEnabled('mobile_app')) {
_createUI();
_fetchCompleter.complete();
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
} else {
_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")]));
}
@ -92,7 +87,7 @@ class HomeAssistant {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool cached = prefs.getBool('cached');
if (cached != null && cached) {
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
if (entities == null) entities = EntityCollection(AppSettings().httpWebHost);
try {
_getStates(prefs);
if (!autoUi) {
@ -194,9 +189,9 @@ class HomeAssistant {
} else {
Completer completer = Completer();
var additionalData;
if (ConnectionManager().haVersion >= 107 && _lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
if (AppSettings().haVersion >= 107 && currentDashboardPath != HomeAssistant.DEFAULT_DASHBOARD) {
additionalData = {
'url_path': _lovelaceDashbordUrl
'url_path': currentDashboardPath
};
}
ConnectionManager().sendSocketMessage(
@ -226,7 +221,7 @@ class HomeAssistant {
var data = json.decode(prefs.getString('cached_services'));
_parseServices(data ?? {});
} catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace);
Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true);
}
}
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
@ -266,7 +261,7 @@ class HomeAssistant {
var data = json.decode(sharedPrefs.getString('cached_panels'));
_parsePanels(data ?? {});
} catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace);
Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true);
panels.clear();
}
} else {
@ -283,6 +278,7 @@ class HomeAssistant {
_rawPanels = data;
List<Panel> dashboards = [];
data.forEach((k,v) {
Logger.d('[HA] Panel $k: title=${v['title']}; component=${v['component_name']}');
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
if (v['component_name'] != null && v['component_name'] == 'lovelace') {
dashboards.add(

View File

@ -16,27 +16,27 @@ import 'package:http/http.dart' as http;
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:device_info/device_info.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'plugins/dynamic_multi_column_layout.dart';
import 'plugins/spoiler_card.dart';
import 'package:workmanager/workmanager.dart' as workManager;
import 'package:geolocator/geolocator.dart';
import 'package:battery/battery.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
import 'package:webview_flutter/webview_flutter.dart';
import 'package:syncfusion_flutter_core/core.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong/latlong.dart';
import 'package:flutter/gestures.dart';
import 'utils/logger.dart';
import '.secrets.dart';
part 'const.dart';
part 'utils/launcher.dart';
part 'utils/RandomColorGenerator.dart';
part 'entities/entity.class.dart';
part 'entities/entity_wrapper.class.dart';
part 'entities/timer/timer_entity.class.dart';
@ -49,6 +49,7 @@ part 'entities/date_time/date_time_entity.class.dart';
part 'entities/light/light_entity.class.dart';
part 'entities/select/select_entity.class.dart';
part 'entities/sun/sun_entity.class.dart';
part 'cards/widgets/entities_map.dart';
part 'entities/sensor/sensor_entity.class.dart';
part 'entities/slider/slider_entity.dart';
part 'entities/media_player/media_player_entity.class.dart';
@ -62,6 +63,7 @@ part 'entities/entity_model.widget.dart';
part 'entities/default_entity_container.widget.dart';
part 'entities/missed_entity.widget.dart';
part 'cards/entity_button_card.dart';
part 'cards/map_card.dart';
part 'pages/widgets/entity_attributes_list.dart';
part 'entities/entity_icon.widget.dart';
part 'entities/entity_name.widget.dart';
@ -118,7 +120,6 @@ part 'pages/entity.page.dart';
part 'utils/mdi.class.dart';
part 'entity_collection.class.dart';
part 'managers/auth_manager.class.dart';
part 'managers/location_manager.class.dart';
part 'managers/mobile_app_integration_manager.class.dart';
part 'managers/connection_manager.class.dart';
part 'managers/device_info_manager.class.dart';
@ -142,6 +143,7 @@ part 'cards/horizontal_srack_card.dart';
part 'cards/markdown_card.dart';
part 'cards/media_control_card.dart';
part 'cards/unsupported_card.dart';
part 'cards/light_card.dart';
part 'cards/error_card.dart';
part 'cards/vertical_stack_card.dart';
part 'cards/glance_card.dart';
@ -153,15 +155,12 @@ part 'pages/whats_new.page.dart';
part 'pages/fullscreen.page.dart';
part 'popups.dart';
part 'cards/badges.dart';
part 'managers/app_settings.dart';
EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = 'HA Client';
const appVersionNumber = '1.0.1';
final String appVersionAdd = secrets['version_type'] ?? '';
final String appVersion = '$appVersionNumber${appVersionAdd.isNotEmpty ? '-' : ''}$appVersionAdd';
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.0.1.md';
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0');
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.3.0.md';
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
// Print the exception to the console.
@ -186,12 +185,17 @@ void main() async {
};
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
AppTheme theme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
await AppSettings().loadStartupSettings();
await Hive.initFlutter();
if (AppSettings().displayMode == DisplayMode.fullscreen) {
SystemChrome.setEnabledSystemUIOverlays([]);
} else {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
}
runZoned(() {
runApp(new HAClientApp(
theme: theme,
theme: AppSettings().appTheme,
));
}, onError: (error, stack) {
_reportError(error, stack);
@ -228,10 +232,6 @@ class _HAClientAppState extends State<HAClientApp> {
_currentTheme = event.theme;
});
});
workManager.Workmanager.initialize(
updateDeviceLocationIsolate,
isInDebugMode: false
);
super.initState();
}
@ -245,6 +245,7 @@ class _HAClientAppState extends State<HAClientApp> {
positiveText: "Ok"
)
));
InAppPurchaseConnection.instance.completePurchase(purchase[0]);
} else {
Logger.d("Purchase change handler: ${purchase[0].status}");
}
@ -253,7 +254,6 @@ class _HAClientAppState extends State<HAClientApp> {
}
}
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
@ -286,7 +286,7 @@ class _HAClientAppState extends State<HAClientApp> {
"/quick-start": (context) => QuickStartPage(),
"/haclient_zha": (context) => ZhaPage(),
"/auth": (context) => new standaloneWebview.WebviewScaffold(
url: "${ConnectionManager().oauthUrl}",
url: "${AppSettings().oauthUrl}",
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.help),

View File

@ -0,0 +1,155 @@
part of '../main.dart';
enum DisplayMode {normal, fullscreen}
class AppSettings {
static const DEFAULT_HIVE_BOX = 'defaultSettingsBox';
static const AUTH_TOKEN_KEY = 'llt';
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
static final AppSettings _instance = AppSettings._internal();
factory AppSettings() {
return _instance;
}
AppSettings._internal();
String mobileAppDeviceName;
String _domain;
String _port;
String displayHostname;
String webSocketAPIEndpoint;
String httpWebHost;
String longLivedToken;
String tempToken;
String oauthUrl;
String webhookId;
double haVersion;
bool scrollBadges;
bool nextAlarmSensorCreated = false;
DisplayMode displayMode;
AppTheme appTheme;
bool get isAuthenticated => longLivedToken != null;
bool get isTempAuthenticated => tempToken != null;
loadStartupSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
appTheme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
displayMode = DisplayMode.values[prefs.getInt('display-mode') ?? DisplayMode.normal.index];
}
Future load(bool full) async {
if (full) {
await Hive.openBox(DEFAULT_HIVE_BOX);
Logger.d('Loading settings...');
SharedPreferences prefs = await SharedPreferences.getInstance();
await migrate(prefs);
_domain = prefs.getString('hassio-domain');
_port = prefs.getString('hassio-port');
webhookId = prefs.getString('app-webhook-id');
mobileAppDeviceName = prefs.getString('app-integration-device-name');
scrollBadges = prefs.getBool('scroll-badges') ?? true;
displayHostname = "$_domain:$_port";
webSocketAPIEndpoint =
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
httpWebHost =
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
longLivedToken = Hive.box(DEFAULT_HIVE_BOX).get(AUTH_TOKEN_KEY);
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'https://ha-client.app')}&redirect_uri=${Uri
.encodeComponent(
'https://ha-client.app/service/auth_callback.html')}";
}
}
Future migrate(SharedPreferences prefs) async {
//Migrating to new location tracking. TODO: Remove when no version 1.2.0 (and older) in the wild
if (prefs.getBool("location-tracking-migrated") == null) {
Logger.d("[MIGRATION] Migrating to new location tracking...");
bool oldLocationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
if (oldLocationTrackingEnabled) {
await platform.invokeMethod('cancelOldLocationWorker');
int oldLocationTrackingInterval = prefs.getInt("location-interval") ?? 0;
if (oldLocationTrackingInterval < 15) {
oldLocationTrackingInterval = 15;
}
try {
await platform.invokeMethod('startLocationService', <String, dynamic>{
'location-updates-interval': oldLocationTrackingInterval * 60 * 1000,
//'location-updates-priority': 100,
'location-updates-show-notification': true,
'foreground-location-tracking': false
});
} catch (e, stack) {
Logger.e("[MIGRATION] Can't start new location tracking: $e", stacktrace: stack);
}
} else {
Logger.d("[MIGRATION] Old location tracking was disabled");
}
await prefs.setBool("location-tracking-migrated", true);
}
//Migrating from integration without next alarm sensor. TODO: remove when no version 1.1.2 (and older) in the wild
nextAlarmSensorCreated = prefs.getBool("next-alarm-sensor-created") ?? false;
//Done
Logger.d("[MIGRATION] Done.");
}
Future<dynamic> loadSingle(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.get('$key');
}
Future save(Map<String, dynamic> settings) async {
if (settings != null && settings.isNotEmpty) {
SharedPreferences prefs = await SharedPreferences.getInstance();
settings.forEach((k,v) async {
if (v is String) {
await prefs.setString(k, v);
} else if (v is bool) {
await prefs.setBool(k ,v);
} else if (v is int) {
await prefs.setInt(k ,v);
} else if (v is double) {
await prefs.setDouble(k, v);
} else {
Logger.e('Unknown setting type: <$k, $v>');
}
});
}
}
Future startAuth() {
return AuthManager().start(
oauthUrl: oauthUrl
).then((token) {
Logger.d("Token from AuthManager recived");
tempToken = token;
});
}
Future clearTokens() async {
longLivedToken = null;
tempToken = null;
Hive.box(DEFAULT_HIVE_BOX).delete(AUTH_TOKEN_KEY);
}
void saveLongLivedToken(token) {
longLivedToken = token;
tempToken = null;
Hive.box(DEFAULT_HIVE_BOX).put(AUTH_TOKEN_KEY, longLivedToken);
}
bool isNotConfigured() {
return _domain == null && _port == null && webhookId == null && mobileAppDeviceName == null;
}
bool isSomethingMissed() {
return (_domain == null) || (_port == null) || (_domain.isEmpty) || (_port.isEmpty);
}
}

View File

@ -10,25 +10,11 @@ class ConnectionManager {
ConnectionManager._internal();
String _domain;
String _port;
String displayHostname;
String _webSocketAPIEndpoint;
String httpWebHost;
String _token;
String _tempToken;
String oauthUrl;
String webhookId;
double haVersion;
bool scrollBadges;
String mobileAppDeviceName;
bool settingsLoaded = false;
int appIntegrationVersion;
bool get isAuthenticated => _token != null;
StreamSubscription _socketSubscription;
Duration connectTimeout = Duration(seconds: 15);
bool isConnected = false;
bool settingsLoaded = false;
var onStateChangeCallback;
var onLovelaceUpdatedCallback;
@ -38,70 +24,27 @@ class ConnectionManager {
int _currentMessageId = 0;
Map<String, Completer> _messageResolver = {};
Future init({bool loadSettings, bool forceReconnect: false}) async {
Future init({bool loadSettings, bool forceReconnect: false}) {
Completer completer = Completer();
bool stopInit = false;
if (loadSettings) {
Logger.d("Loading settings...");
SharedPreferences prefs = await SharedPreferences.getInstance();
_domain = prefs.getString('hassio-domain');
_port = prefs.getString('hassio-port');
webhookId = prefs.getString('app-webhook-id');
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
mobileAppDeviceName = prefs.getString('app-integration-device-name');
scrollBadges = prefs.getBool('scroll-badges') ?? true;
displayHostname = "$_domain:$_port";
_webSocketAPIEndpoint =
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
httpWebHost =
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
Logger.d('$_domain$_port');
if (_domain == null && _port == null && webhookId == null && mobileAppDeviceName == null) {
AppSettings().load(loadSettings).then((_) {
Logger.d('Checking config...');
if (AppSettings().isNotConfigured()) {
Logger.d('This is first start');
completer.completeError(HACNotSetUpException());
stopInit = true;
} else if ((_domain == null) || (_port == null) ||
(_domain.isEmpty) || (_port.isEmpty)) {
} else if (AppSettings().isSomethingMissed()) {
completer.completeError(HACException.checkConnectionSettings());
stopInit = true;
} else {
final storage = new FlutterSecureStorage();
try {
_token = await storage.read(key: "hacl_llt");
Logger.d("Long-lived token read successful");
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'https://ha-client.app')}&redirect_uri=${Uri
.encodeComponent(
'https://ha-client.app/service/auth_callback.html')}";
settingsLoaded = true;
} catch (e, stacktrace) {
completer.completeError(HACException("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
Logger.e("Error reading secure storage: $e", stacktrace: stacktrace);
stopInit = true;
}
}
} else {
if ((_domain == null) || (_port == null) ||
(_domain.isEmpty) || (_port.isEmpty)) {
completer.completeError(HACException.checkConnectionSettings());
stopInit = true;
}
}
if (!stopInit) {
if (_token == null) {
AuthManager().start(
oauthUrl: oauthUrl
).then((token) {
Logger.d("Token from AuthManager recived");
_tempToken = token;
} else if (!AppSettings().isAuthenticated) {
settingsLoaded = true;
AppSettings().startAuth().then((_) {
_doConnect(completer: completer, forceReconnect: forceReconnect);
}).catchError((e) {
completer.completeError(e);
});
} else {
settingsLoaded = true;
_doConnect(completer: completer, forceReconnect: forceReconnect);
}
}
}
});
return completer.future;
}
@ -143,7 +86,7 @@ class ConnectionManager {
Logger.d("Socket connecting...");
try {
_socket = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
AppSettings().webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
_socketSubscription = _socket.stream.listen(
(message) {
isConnected = true;
@ -159,9 +102,9 @@ class ConnectionManager {
} else if (data["type"] == "auth_ok") {
String v = data["ha_version"];
if (v != null && v.isNotEmpty) {
haVersion = double.tryParse(v.replaceFirst('0.','')) ?? 0;
AppSettings().haVersion = double.tryParse(v.replaceFirst('0.','')) ?? 0;
}
Logger.d("Home assistant version: $v ($haVersion)");
Logger.d("Home assistant version: $v (${AppSettings().haVersion})");
Crashlytics.instance.setString('ha_version', v);
Logger.d("[Connection] Subscribing to events");
sendSocketMessage(
@ -174,7 +117,7 @@ class ConnectionManager {
).whenComplete((){
_messageResolver["auth"]?.complete();
_messageResolver.remove("auth");
if (_token != null) {
if (AppSettings().isAuthenticated) {
if (!connecting.isCompleted) connecting.complete();
}
});
@ -268,21 +211,21 @@ class ConnectionManager {
Future _authenticate() {
Completer completer = Completer();
if (_token != null) {
if (AppSettings().isAuthenticated) {
Logger.d( "Long-lived token exist");
Logger.d( "[Sending] ==> auth request");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_token"},
additionalData: {"access_token": "${AppSettings().longLivedToken}"},
auth: true
).then((_) {
completer.complete();
}).catchError((e) => completer.completeError(e));
} else if (_tempToken != null) {
} else if (AppSettings().isTempAuthenticated != null) {
Logger.d("We have temp token. Loging in...");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_tempToken"},
additionalData: {"access_token": "${AppSettings().tempToken}"},
auth: true
).then((_) {
Logger.d("Requesting long-lived token...");
@ -300,35 +243,18 @@ class ConnectionManager {
return completer.future;
}
Future logout() {
Future logout() async {
Logger.d("Logging out");
Completer completer = Completer();
_disconnect().whenComplete(() {
_token = null;
_tempToken = null;
final storage = new FlutterSecureStorage();
storage.delete(key: "hacl_llt").whenComplete((){
completer.complete();
});
});
return completer.future;
await _disconnect();
await AppSettings().clearTokens();
}
Future _getLongLivedToken() {
Completer completer = Completer();
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
Logger.d("Got long-lived token.");
_token = data;
_tempToken = null;
final storage = new FlutterSecureStorage();
storage.write(key: "hacl_llt", value: "$_token").then((_) {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool("oauth-used", true);
completer.complete();
});
}).catchError((e) {
throw e;
});
AppSettings().saveLongLivedToken(data);
completer.complete();
}).catchError((e) {
completer.completeError(HACException("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
});
@ -400,11 +326,11 @@ class ConnectionManager {
DateTime now = DateTime.now();
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String url = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
String url = "${AppSettings().httpWebHost}/api/history/period/$startTime?&filter_entity_id=$entityId";
Logger.d("[Sending] ==> HTTP /api/history/period/$startTime?&filter_entity_id=$entityId");
http.Response historyResponse;
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_token",
"authorization": "Bearer ${AppSettings().longLivedToken}",
"Content-Type": "application/json"
});
var history = json.decode(historyResponse.body);
@ -418,14 +344,14 @@ class ConnectionManager {
Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async {
Completer completer = Completer();
String url = "$httpWebHost$endPoint";
String url = "${AppSettings().httpWebHost}$endPoint";
Logger.d("[Sending] ==> HTTP $endPoint");
Map<String, String> headers = {};
if (contentType != null) {
headers["Content-Type"] = contentType;
}
if (includeAuthHeader) {
headers["authorization"] = "Bearer $_token";
headers["authorization"] = "Bearer ${AppSettings().longLivedToken}";
}
http.post(
url,

View File

@ -1,244 +0,0 @@
part of '../main.dart';
class LocationManager {
static final LocationManager _instance = LocationManager
._internal();
factory LocationManager() {
return _instance;
}
LocationManager._internal() {
init();
}
final int defaultUpdateIntervalMinutes = 20;
final String backgroundTaskId = "haclocationtask0";
final String backgroundTaskTag = "haclocation";
Duration _updateInterval;
bool _isRunning;
void init() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload();
_updateInterval = Duration(minutes: prefs.getInt("location-interval") ??
defaultUpdateIntervalMinutes);
_isRunning = prefs.getBool("location-enabled") ?? false;
if (_isRunning) {
await _startLocationService();
}
}
setSettings(bool enabled, int interval) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
if (interval != _updateInterval.inMinutes) {
prefs.setInt("location-interval", interval);
_updateInterval = Duration(minutes: interval);
if (_isRunning) {
Logger.d("Stopping location tracking...");
_isRunning = false;
await _stopLocationService();
}
}
if (enabled && !_isRunning) {
Logger.d("Starting location tracking");
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool("location-enabled", enabled);
_isRunning = true;
await _startLocationService();
} else if (!enabled && _isRunning) {
Logger.d("Stopping location tracking...");
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool("location-enabled", enabled);
_isRunning = false;
await _stopLocationService();
}
}
_startLocationService() async {
String webhookId = ConnectionManager().webhookId;
String httpWebHost = ConnectionManager().httpWebHost;
if (webhookId != null && webhookId.isNotEmpty) {
Duration interval;
int delayFactor;
int taskCount;
Logger.d("Starting location update for every ${_updateInterval
.inMinutes} minutes...");
if (_updateInterval.inMinutes == 10) {
interval = Duration(minutes: 20);
taskCount = 2;
delayFactor = 10;
} else if (_updateInterval.inMinutes == 5) {
interval = Duration(minutes: 15);
taskCount = 3;
delayFactor = 5;
} else {
interval = _updateInterval;
taskCount = 1;
delayFactor = 0;
}
for (int i = 1; i <= taskCount; i++) {
int delay = i*delayFactor;
Logger.d("Scheduling location update task #$i for every ${interval.inMinutes} minutes in $delay minutes...");
await workManager.Workmanager.registerPeriodicTask(
"$backgroundTaskId$i",
"haClientLocationTracking-0$i",
tag: backgroundTaskTag,
inputData: {
"webhookId": webhookId,
"httpWebHost": httpWebHost
},
frequency: interval,
initialDelay: Duration(minutes: delay),
existingWorkPolicy: workManager.ExistingWorkPolicy.keep,
backoffPolicy: workManager.BackoffPolicy.linear,
backoffPolicyDelay: interval,
constraints: workManager.Constraints(
networkType: workManager.NetworkType.connected,
),
);
}
}
}
_stopLocationService() async {
Logger.d("Canceling previous schedule if any...");
await workManager.Workmanager.cancelAll();
}
updateDeviceLocation() async {
try {
Logger.d("[Foreground location] Started");
Geolocator geolocator = Geolocator();
var battery = Battery();
String webhookId = ConnectionManager().webhookId;
String httpWebHost = ConnectionManager().httpWebHost;
if (webhookId != null && webhookId.isNotEmpty) {
Logger.d("[Foreground location] Getting battery level...");
int batteryLevel = await battery.batteryLevel;
Logger.d("[Foreground location] Getting device location...");
Position position = await geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
locationPermissionLevel: GeolocationPermission.locationAlways
);
if (position != null) {
Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})");
String url = "$httpWebHost/api/webhook/$webhookId";
Map data = {
"type": "update_location",
"data": {
"gps": [position.latitude, position.longitude],
"gps_accuracy": position.accuracy,
"battery": batteryLevel ?? 100
}
};
Logger.d("[Foreground location] Sending data home...");
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}");
} else {
Logger.d("[Foreground location] No location. Aborting.");
}
}
} catch (e, stack) {
Logger.e('Foreground location error: ${e.toSTring()}', stacktrace: stack);
}
}
}
void updateDeviceLocationIsolate() {
workManager.Workmanager.executeTask((backgroundTask, data) async {
//print("[Background $backgroundTask] Started");
Geolocator geolocator = Geolocator();
var battery = Battery();
String webhookId = data["webhookId"];
String httpWebHost = data["httpWebHost"];
//String logData = '==> ${DateTime.now()} [Background $backgroundTask]:';
//print("[Background $backgroundTask] Getting path for log file...");
//final logFileDirectory = await getExternalStorageDirectory();
//print("[Background $backgroundTask] Opening log file...");
//File logFile = File('${logFileDirectory.path}/ha-client-background-log.txt');
//print("[Background $backgroundTask] Log file path: ${logFile.path}");
if (webhookId != null && webhookId.isNotEmpty) {
String url = "$httpWebHost/api/webhook/$webhookId";
Map<String, String> headers = {};
headers["Content-Type"] = "application/json";
Map data = {
"type": "update_location",
"data": {
"gps": [],
"gps_accuracy": 0,
"battery": 100
}
};
//print("[Background $backgroundTask] Getting battery level...");
int batteryLevel;
try {
batteryLevel = await battery.batteryLevel;
//print("[Background $backgroundTask] Got battery level: $batteryLevel");
} catch(e) {
//print("[Background $backgroundTask] Error getting battery level: $e. Setting zero");
batteryLevel = 0;
//logData += 'Battery: error, $e';
}
if (batteryLevel != null) {
data["data"]["battery"] = batteryLevel;
//logData += 'Battery: success, $batteryLevel';
}/* else {
logData += 'Battery: error, level is null';
}*/
Position location;
try {
location = await geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways);
if (location != null && location.latitude != null) {
//logData += ' || Location: success, ${location.latitude} ${location.longitude} (${location.timestamp})';
data["data"]["gps"] = [location.latitude, location.longitude];
data["data"]["gps_accuracy"] = location.accuracy;
try {
http.Response response = await http.post(
url,
headers: headers,
body: json.encode(data)
);
/*if (response.statusCode >= 200 && response.statusCode < 300) {
logData += ' || Post: success, ${response.statusCode}';
} else {
logData += ' || Post: error, ${response.statusCode}';
}*/
} catch(e) {
//logData += ' || Post: error, $e';
}
}/* else {
logData += ' || Location: error, location is null';
}*/
} catch (e) {
//print("[Background $backgroundTask] Location error: $e");
//logData += ' || Location: error, $e';
}
}/* else {
logData += 'Not configured';
}*/
//print("[Background $backgroundTask] Writing log data...");
/*try {
var fileMode;
if (logFile.existsSync() && logFile.lengthSync() < 5000000) {
fileMode = FileMode.append;
} else {
fileMode = FileMode.write;
}
await logFile.writeAsString('$logData\n', mode: fileMode);
} catch (e) {
print("[Background $backgroundTask] Error writing log: $e");
}
print("[Background $backgroundTask] Finished.");*/
return true;
});
}

View File

@ -2,17 +2,15 @@ part of '../main.dart';
class MobileAppIntegrationManager {
static const INTEGRATION_VERSION = 3;
static final _appRegistrationData = {
"device_name": "",
"app_version": "$appVersion",
"manufacturer": DeviceInfoManager().manufacturer,
"model": DeviceInfoManager().model,
"os_version": DeviceInfoManager().osVersion,
"manufacturer": DeviceInfoManager().manufacturer ?? "unknown",
"model": DeviceInfoManager().model ?? "unknown",
"os_version": DeviceInfoManager().osVersion ?? "0",
"app_data": {
"push_token": "",
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV3"
}
};
@ -23,11 +21,29 @@ class MobileAppIntegrationManager {
return '${HomeAssistant().userName}\'s ${DeviceInfoManager().model}';
}
static Future checkAppRegistration() {
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
static Future checkAppRegistration() async {
String fcmToken = await AppSettings().loadSingle('npush-token');
if (fcmToken != null) {
Logger.d("[MobileAppIntegrationManager] token exist");
await _doCheck(fcmToken);
} else {
Logger.d("[MobileAppIntegrationManager] no fcm token. Requesting...");
try {
fcmToken = await platform.invokeMethod('getFCMToken');
await _doCheck(fcmToken);
} on PlatformException catch (e) {
Logger.e('[MobileAppIntegrationManager] Failed to get FCM token from native: ${e.message}');
}
}
}
static Future _doCheck(String fcmToken) {
Completer completer = Completer();
_appRegistrationData["device_name"] = ConnectionManager().mobileAppDeviceName ?? getDefaultDeviceName();
(_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
if (ConnectionManager().webhookId == null) {
_appRegistrationData["device_name"] = AppSettings().mobileAppDeviceName ?? getDefaultDeviceName();
(_appRegistrationData["app_data"] as Map)["push_token"] = "$fcmToken";
if (AppSettings().webhookId == null) {
Logger.d("Mobile app was not registered yet. Registering...");
var registrationData = Map.from(_appRegistrationData);
registrationData.addAll({
@ -36,7 +52,7 @@ class MobileAppIntegrationManager {
"os_name": DeviceInfoManager().osName,
"supports_encryption": false,
});
if (ConnectionManager().haVersion >= 104) {
if (AppSettings().haVersion >= 104) {
registrationData.addAll({
"device_id": "${DeviceInfoManager().unicDeviceId}"
});
@ -46,14 +62,13 @@ class MobileAppIntegrationManager {
includeAuthHeader: true,
data: json.encode(registrationData)
).then((response) {
Logger.d("Processing registration responce...");
Logger.d("Processing registration response...");
var responseObject = json.decode(response);
ConnectionManager().webhookId = responseObject["webhook_id"];
ConnectionManager().appIntegrationVersion = INTEGRATION_VERSION;
SharedPreferences.getInstance().then((prefs) {
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
prefs.setInt('app-integration-version', INTEGRATION_VERSION);
AppSettings().webhookId = responseObject["webhook_id"];
AppSettings().save({
'app-webhook-id': responseObject["webhook_id"]
}).then((_) {
_createNextAlarmSensor(true);
completer.complete();
eventBus.fire(ShowPopupEvent(
popup: Popup(
@ -76,7 +91,6 @@ class MobileAppIntegrationManager {
}
_showError();
});
return completer.future;
} else {
Logger.d("App was previously registered. Checking...");
var updateData = {
@ -84,7 +98,7 @@ class MobileAppIntegrationManager {
"data": _appRegistrationData
};
ConnectionManager().sendHTTPPost(
endPoint: "/api/webhook/${ConnectionManager().webhookId}",
endPoint: "/api/webhook/${AppSettings().webhookId}",
includeAuthHeader: false,
data: json.encode(updateData)
).then((response) {
@ -98,12 +112,8 @@ class MobileAppIntegrationManager {
Logger.w("No registration data in response. MobileApp integration was removed or broken");
_askToRegisterApp();
} else {
if (INTEGRATION_VERSION > ConnectionManager().appIntegrationVersion) {
Logger.d('App registration needs to be updated');
_askToRemoveAndRegisterApp();
} else {
Logger.d('App registration works fine');
}
Logger.d('App registration works fine');
_createNextAlarmSensor(false);
}
completer.complete();
}).catchError((e) {
@ -119,8 +129,44 @@ class MobileAppIntegrationManager {
}
completer.complete();
});
return completer.future;
}
return completer.future;
}
static _createNextAlarmSensor(bool force) {
if (AppSettings().nextAlarmSensorCreated && !force) {
Logger.d("Next alarm sensor was previously created");
return;
}
Logger.d("Creating next alarm sensor...");
ConnectionManager().sendHTTPPost(
endPoint: "/api/webhook/${AppSettings().webhookId}",
includeAuthHeader: false,
data: json.encode(
{
"data": {
"device_class": "timestamp",
"icon": "mdi:alarm",
"name": "Next Alarm",
"state": "",
"type": "sensor",
"unique_id": "next_alarm"
},
"type": "register_sensor"
}
)
).then((_){
AppSettings().nextAlarmSensorCreated = true;
AppSettings().save({
'next-alarm-sensor-created': true
});
}).catchError((e) {
if (e is http.Response) {
Logger.e("Error creating next alarm sensor: ${e.statusCode}: ${e.body}");
} else {
Logger.e("Error creating next alarm sensor: ${e?.toString()}");
}
});
}
static void _showError() {
@ -128,28 +174,11 @@ class MobileAppIntegrationManager {
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",
positiveText: "Report issue",
negativeText: "Close",
onPositive: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
},
onNegative: () {
Launcher.launchURLInBrowser("https://discord.gg/u9vq7QE");
},
)
));
}
static void _askToRemoveAndRegisterApp() {
eventBus.fire(ShowPopupEvent(
popup: Popup(
title: "Mobile app integration needs to be updated",
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.",
positiveText: "Ok",
negativeText: "Report an issue",
onNegative: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
},
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose");
}
)
));
}

View File

@ -11,19 +11,27 @@ class StartupUserMessagesManager {
StartupUserMessagesManager._internal();
bool _supportAppDevelopmentMessageShown;
bool _needToshowDonateMessage;
bool _whatsNewMessageShown;
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
static final _donateMsgTimerKey = "user-msg-donate-timer";
static final _donateMsgShownKey = "user-msg-donate-shpown";
static final _whatsNewMessageKey = "user-msg-whats-new-url";
void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload();
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
var tInt = prefs.getInt(_donateMsgTimerKey);
if (tInt == null) {
prefs.setInt(_donateMsgTimerKey, DateTime.now().millisecondsSinceEpoch);
_needToshowDonateMessage = false;
} else {
bool wasShown = prefs.getBool(_donateMsgShownKey) ?? false;
_needToshowDonateMessage = (Duration(milliseconds: DateTime.now().millisecondsSinceEpoch - tInt).inDays >= 14) && !wasShown;
}
_whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl;
if (!_whatsNewMessageShown) {
_showWhatsNewMessage();
} else if (!_supportAppDevelopmentMessageShown) {
} else if (_needToshowDonateMessage) {
_showSupportAppDevelopmentMessage();
}
}
@ -34,16 +42,16 @@ class StartupUserMessagesManager {
title: "Hi!",
body: "As you may have noticed this app contains no ads. Also all app features are available for you for free. I'm not planning to change this in nearest future, but still you can support this application development materially. There is one-time payment available as well as several subscription options. Thanks.",
positiveText: "Show options",
negativeText: "Cancel",
negativeText: "Later",
onPositive: () {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_supportAppDevelopmentMessageKey, true);
prefs.setBool(_donateMsgShownKey, true);
eventBus.fire(ShowPageEvent(path: "/putchase"));
});
},
onNegative: () {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_supportAppDevelopmentMessageKey, true);
prefs.setInt(_donateMsgTimerKey, DateTime.now().millisecondsSinceEpoch);
});
}
)

View File

@ -1,18 +1,37 @@
part of '../main.dart';
class FullScreenPage extends StatelessWidget {
class FullScreenPage extends StatefulWidget {
final Widget child;
const FullScreenPage({Key key, this.child}) : super(key: key);
@override
_FullScreenPageState createState() => _FullScreenPageState();
}
class _FullScreenPageState extends State<FullScreenPage> {
@override
void initState() {
SystemChrome.setEnabledSystemUIOverlays([]);
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black,
child: Center(
child: this.child,
child: this.widget.child,
),
);
}
@override
void dispose() {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
super.dispose();
}
}

View File

@ -33,35 +33,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
super.initState();
WidgetsBinding.instance.addObserver(this);
_firebaseMessaging.configure(
onLaunch: (data) {
Logger.d("Notification [onLaunch]: $data");
return Future.value();
},
onMessage: (data) {
Logger.d("Notification [onMessage]: $data");
return _showNotification(title: data["notification"]["title"], text: data["notification"]["body"]);
},
onResume: (data) {
Logger.d("Notification [onResume]: $data");
return Future.value();
}
);
_bottomInfoBarController = BottomInfoBarController();
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
var initializationSettingsAndroid =
new AndroidInitializationSettings('mini_icon');
var initializationSettingsIOS = new IOSInitializationSettings(
onDidReceiveLocalNotification: null);
var initializationSettings = new InitializationSettings(
initializationSettingsAndroid, initializationSettingsIOS);
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: onSelectNotification);
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
Logger.d("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) {
@ -73,36 +46,18 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_fullLoad();
}
Future onSelectNotification(String payload) async {
if (payload != null) {
Logger.d('Notification clicked: ' + payload);
}
}
Future _showNotification({String title, String text}) async {
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service',
importance: Importance.Max, priority: Priority.High);
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
var platformChannelSpecifics = new NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
title ?? appName,
text,
platformChannelSpecifics
);
}
void _fullLoad() {
_bottomInfoBarController.showInfoBottomBar(progress: true,);
Logger.d('[loading] fullLoad');
_subscribe().then((_) {
Logger.d('[loading] subscribed');
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
Logger.d('[loading] COnnection manager initialized');
SharedPreferences.getInstance().then((prefs) {
HomeAssistant().lovelaceDashboardUrl = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
HomeAssistant().currentDashboardPath = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
_fetchData(useCache: true);
LocationManager();
StartupUserMessagesManager().checkMessagesToShow();
MobileAppIntegrationManager.checkAppRegistration();
});
}, onError: (e) {
if (e is HACNotSetUpException) {
@ -154,9 +109,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}
}
Future _subscribe() {
Completer completer = Completer();
Future _subscribe() async {
if (_stateSubscription == null) {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) {
@ -237,16 +190,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
});
}
_firebaseMessaging.getToken().then((String token) {
/*_firebaseMessaging.getToken().then((String token) {
HomeAssistant().fcmToken = token;
completer.complete();
});
return completer.future;
});*/
}
void _showOAuth() {
_preventAppRefresh = true;
Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
Navigator.of(context).pushNamed("/auth", arguments: {"url": AppSettings().oauthUrl});
}
_setErrorState(HACException e) {
@ -348,7 +300,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
title: Text("Report an issue"),
onTap: () {
Navigator.of(context).pop();
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose");
},
),
Divider(),
@ -449,82 +401,37 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> serviceMenuItems = [];
List<PopupMenuItem<String>> mediaMenuItems = [];
List<Entity> activePlayers = [];
List<Entity> activeLights = [];
Color mediaMenuIconColor;
Color lightMenuIconColor;
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
if (_previousViewCount != currentViewCount) {
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
//Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
_viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount;
}
serviceMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
));
if (ConnectionManager().isAuthenticated) {
if (AppSettings().isAuthenticated) {
_showLoginButton = false;
serviceMenuItems.add(
PopupMenuItem<String>(
child: new Text("Logout"),
value: "logout",
));
}
Widget mediaMenuIcon;
int playersCount = 0;
if (!empty && !HomeAssistant().entities.isEmpty) {
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
playersCount = activePlayers.length;
mediaMenuItems.addAll(
activePlayers.map((entity) => PopupMenuItem<String>(
child: Text(
"${entity.displayName}",
style: Theme.of(context).textTheme.body1.copyWith(
color: HAClientTheme().getColorByEntityState(entity.state, context)
)
),
value: "${entity.entityId}",
)).toList()
);
activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
activeLights = HomeAssistant().entities.getByDomains(includeDomains: ["light"], stateFiler: [EntityState.on]);
}
mediaMenuItems.addAll([
PopupMenuItem<String>(
child: new Text("Play media..."),
value: "play_media",
)
]);
if (playersCount > 0) {
mediaMenuIcon = Stack(
overflow: Overflow.visible,
children: <Widget>[
Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:television"), color: Colors.white,),
Positioned(
bottom: -4,
right: -4,
child: Container(
height: 16,
width: 16,
decoration: new BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
child: Center(
child: Text(
"$playersCount",
style: Theme.of(context).textTheme.caption.copyWith(
color: Colors.white
)
),
),
),
)
],
);
if (activePlayers.isNotEmpty) {
mediaMenuIconColor = Theme.of(context).accentColor;
} else {
mediaMenuIcon = Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:television"), color: Colors.white,);
mediaMenuIconColor = Theme.of(context).primaryIconTheme.color;
}
if (activeLights.isNotEmpty) {
lightMenuIconColor = Theme.of(context).accentColor;
} else {
lightMenuIconColor = Theme.of(context).primaryIconTheme.color;
}
Widget mainScrollBody;
if (empty) {
@ -575,46 +482,43 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
SliverAppBar(
floating: true,
pinned: true,
primary: true,
snap: false,
primary: AppSettings().displayMode == DisplayMode.normal,
title: Text(HomeAssistant().locationName ?? ""),
actions: <Widget>[
IconButton(
icon: mediaMenuIcon,
onPressed: () {
showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, MediaQuery.of(context).padding.top + Sizes.iconSize + 20, 50, 0),
context: context,
items: mediaMenuItems
).then((String val) {
if (val == "play_media") {
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
} else if (val != null) {
_showEntityPage(val);
}
PopupMenuButton(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 15),
child: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Theme.of(context).primaryIconTheme.color)
),
onSelected: (String val) {
if (val == "reload") {
_quickLoad();
} else if (val == "logout") {
HomeAssistant().logout().then((_) {
_quickLoad();
});
}
},
itemBuilder: (BuildContext context) {
List<PopupMenuEntry<String>> result = [
PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
)
];
if (AppSettings().isAuthenticated) {
result.addAll([
PopupMenuDivider(),
PopupMenuItem<String>(
child: new Text("Logout"),
value: "logout",
)]);
}
return result;
},
),
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Colors.white,),
onPressed: () {
showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, MediaQuery.of(context).padding.top + Sizes.iconSize + 20, 0.0, 0.0),
context: context,
items: serviceMenuItems
).then((String val) {
HomeAssistant().lovelaceDashboardUrl = HomeAssistant.DEFAULT_DASHBOARD;
if (val == "reload") {
_quickLoad();
} else if (val == "logout") {
HomeAssistant().logout().then((_) {
_quickLoad();
});
}
});
}
)
],
leading: IconButton(
icon: Icon(Icons.menu),
@ -622,6 +526,99 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_scaffoldKey.currentState.openDrawer();
},
),
expandedHeight: 130,
flexibleSpace: FlexibleSpaceBar(
title: Padding(
padding: EdgeInsets.only(bottom: 15),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PopupMenuButton<String>(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
child: Icon(
MaterialDesignIcons.getIconDataFromIconName("mdi:television"),
color: mediaMenuIconColor,
size: 20,
)
),
onSelected: (String val) {
if (val == "play_media") {
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
} else if (val != null) {
_showEntityPage(val);
}
},
itemBuilder: (BuildContext context) {
List<PopupMenuEntry<String>> result = [
PopupMenuDivider(),
PopupMenuItem<String>(
child: new Text("Play media..."),
value: "play_media",
)
];
if (activePlayers.isNotEmpty) {
result.insertAll(0,
activePlayers.map((entity) => PopupMenuItem<String>(
child: Text(
"${entity.displayName}",
style: Theme.of(context).textTheme.body1.copyWith(
color: HAClientTheme().getColorByEntityState(entity.state, context)
)
),
value: "${entity.entityId}",
)).toList()
);
} else {
result.insert(0, PopupMenuItem<String>(
child: new Text("No active players"),
value: "_",
enabled: false,
));
}
return result;
}
),
PopupMenuButton<String>(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
child: Icon(
MaterialDesignIcons.getIconDataFromIconName("mdi:lightbulb-outline"),
color: lightMenuIconColor,
size: 20
)
),
onSelected: (String val) {
if (val == 'turn_off_all') {
ConnectionManager().callService(
service: 'turn_off',
domain: 'light',
entityId: 'all'
);
} else if (val == 'turn_on_all') {
ConnectionManager().callService(
service: 'turn_on',
domain: 'light',
entityId: 'all'
);
}
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
child: new Text("Turn on all lights"),
value: "turn_on_all",
),
PopupMenuItem<String>(
child: new Text("Turn off all ligts"),
value: "turn_off_all",
enabled: activeLights.isNotEmpty,
)
],
)
],
) ),
centerTitle: true,
),
bottom: empty ? null : TabBar(
controller: _viewsTabController,
tabs: buildUIViewTabs(),
@ -665,6 +662,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
Hive.close();
//final flutterWebviewPlugin = new FlutterWebviewPlugin();
//flutterWebviewPlugin.dispose();
_viewsTabController?.dispose();

View File

@ -31,7 +31,7 @@ class _PurchasePageState extends State<PurchasePage> {
} else {
const Set<String> _kIds = {'one_time_support','just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
if (!response.notFoundIDs.isEmpty) {
if (response.notFoundIDs.isNotEmpty) {
Logger.d("Products not found: ${response.notFoundIDs}");
}
_products = response.productDetails;
@ -63,7 +63,18 @@ class _PurchasePageState extends State<PurchasePage> {
}
List<Widget> _buildProducts() {
List<Widget> productWidgets = [];
List<Widget> productWidgets = [
Card(
child: Padding(
padding: EdgeInsets.all(15),
child: Text(
'This will not unlock any additional functionality. This is only a donation to the HA Client open source project.',
style: Theme.of(context).textTheme.headline5,
textAlign: TextAlign.center,
)
)
)
];
for (ProductDetails product in _products) {
productWidgets.add(
ProductPurchase(
@ -90,22 +101,6 @@ class _PurchasePageState extends State<PurchasePage> {
} else {
body = _buildProducts();
}
body.add(
Card(
child: Container(
height: 80,
child: InkWell(
child: Image.network('https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif'),
onTap: () {
Launcher.launchURLInCustomTab(
context: context,
url: 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ARWGETZD2D83Q&source=url'
);
},
)
),
)
);
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){

View File

@ -23,7 +23,7 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
Widget _buildMenuItem(BuildContext context, IconData icon,String title, AppSettingsSection section) {
return ListTile(
title: Text(title, style: Theme.of(context).textTheme.subhead),
title: Text(title),
leading: Icon(icon),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () {
@ -39,7 +39,7 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
_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) {
if (AppSettings().webhookId != null) {
items.insert(1, _buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings));
}
return ListView(

View File

@ -31,7 +31,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
}
_loadSettings() async {
_includeDeviceName = widget.quickStart || ConnectionManager().webhookId == null;
_includeDeviceName = widget.quickStart || AppSettings().webhookId == null;
SharedPreferences prefs = await SharedPreferences.getInstance();
String domain = prefs.getString('hassio-domain')?? '';
String port = prefs.getString('hassio-port') ?? '';

View File

@ -11,9 +11,18 @@ class IntegrationSettingsPage extends StatefulWidget {
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
int _locationInterval = LocationManager().defaultUpdateIntervalMinutes;
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
/*static final locationAccuracy = {
100: "High",
102: "Balanced"
};*/
Duration _locationInterval;
bool _locationTrackingEnabled = false;
bool _wait = false;
bool _showNotification = true;
//int _accuracy = 100;
bool _useForegroundService = false;
@override
void initState() {
@ -27,102 +36,235 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
await prefs.reload();
SharedPreferences.getInstance().then((prefs) {
setState(() {
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
_locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
if (_locationInterval % 5 != 0) {
_locationInterval = 5 * (_locationInterval ~/ 5);
}
//_accuracy = prefs.getInt("location-updates-priority") ?? 100;
_locationTrackingEnabled = (prefs.getInt("location-updates-state") ?? 0) > 0;
_showNotification = prefs.getBool("location-updates-show-notification") ?? true;
_useForegroundService = prefs.getBool("foreground-location-tracking") ?? false;
_locationInterval = Duration(milliseconds: prefs.getInt("location-updates-interval") ??
900000);
});
});
}
void _incLocationInterval() {
if (_locationInterval < 720) {
if (_locationInterval.inSeconds < 60) {
setState(() {
_locationInterval = _locationInterval + 5;
_locationInterval = _locationInterval + Duration(seconds: 5);
});
} else if (_locationInterval.inMinutes < 15) {
setState(() {
_locationInterval = _locationInterval + Duration(minutes: 1);
});
} else if (_locationInterval.inMinutes < 60) {
setState(() {
_locationInterval = _locationInterval + Duration(minutes: 5);
});
} else if (_locationInterval.inHours < 4) {
setState(() {
_locationInterval = _locationInterval + Duration(minutes: 10);
});
} else if (_locationInterval.inHours < 48) {
setState(() {
_locationInterval = _locationInterval + Duration(hours: 1);
});
}
}
void _decLocationInterval() {
if (_locationInterval > 5) {
if ((_useForegroundService && _locationInterval.inSeconds > 5) || (!_useForegroundService && _locationInterval.inMinutes > 15)) {
setState(() {
_locationInterval = _locationInterval - 5;
if (_locationInterval.inSeconds <= 60) {
_locationInterval = _locationInterval - Duration(seconds: 5);
} else if (_locationInterval.inMinutes <= 15) {
_locationInterval = _locationInterval - Duration(minutes: 1);
} else if (_locationInterval.inMinutes <= 60) {
_locationInterval = _locationInterval - Duration(minutes: 5);
} else if (_locationInterval.inHours <= 4) {
_locationInterval = _locationInterval - Duration(minutes: 10);
} else if (_locationInterval.inHours > 4) {
_locationInterval = _locationInterval - Duration(hours: 1);
}
});
}
}
_switchLocationTrackingState(bool state) async {
if (state) {
await LocationManager().updateDeviceLocation();
try {
await platform.invokeMethod('startLocationService', <String, dynamic>{
'location-updates-interval': _locationInterval.inMilliseconds,
//'location-updates-priority': _accuracy,
'foreground-location-tracking': _useForegroundService,
'location-updates-show-notification': _showNotification
});
} catch (e) {
_locationTrackingEnabled = false;
}
} else {
await platform.invokeMethod('stopLocationService');
}
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
setState(() {
_wait = false;
});
}
String _formatInterval() {
String result = "";
Duration leftToShow = Duration(seconds: _locationInterval?.inSeconds ?? 0);
if (leftToShow.inHours > 0) {
result += "${leftToShow.inHours} h ";
leftToShow -= Duration(hours: leftToShow.inHours);
}
if (leftToShow.inMinutes > 0) {
result += "${leftToShow.inMinutes} m";
leftToShow -= Duration(hours: leftToShow.inMinutes);
}
if (leftToShow.inSeconds > 0) {
result += "${leftToShow.inSeconds} s";
leftToShow -= Duration(hours: leftToShow.inSeconds);
}
return result;
}
Widget _getNoteWidget(String text, bool important) {
return Text(
text,
style: important ? Theme.of(context).textTheme.caption.copyWith(color: Theme.of(context).errorColor) : Theme.of(context).textTheme.caption,
softWrap: true,
);
}
Widget _getNotes() {
List<Widget> notes = [];
if (_locationTrackingEnabled) {
notes.add(_getNoteWidget('* Stop location tracking to change settings', false));
}
if (_useForegroundService) {
notes.add(_getNoteWidget('* Notification is mandatory for foreground service', false));
} else {
notes.add(_getNoteWidget('* Use foreground service for intervals less then 15 minutes', false));
}
if (_useForegroundService && _locationInterval.inMinutes < 10) {
notes.add(_getNoteWidget('* Battery consumption will be noticeable', true));
}
if (notes.isEmpty) {
return Container(width: 0, height: 0);
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: notes,
);
}
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
Text("Location tracking", style: Theme.of(context).textTheme.title),
Container(height: Sizes.rowPadding,),
InkWell(
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#location-tracking"),
child: Text(
"Please read documentation!",
style: Theme.of(context).textTheme.subhead.copyWith(
color: Colors.blue,
decoration: TextDecoration.underline
)
),
),
Container(height: Sizes.rowPadding,),
Row(
children: <Widget>[
Text("Enable device location tracking"),
Switch(
value: _locationTrackingEnabled,
onChanged: _wait ? null : (value) {
setState(() {
_locationTrackingEnabled = value;
_wait = true;
});
_switchLocationTrackingState(value);
},
),
],
),
Container(height: Sizes.rowPadding,),
Text("Location update interval in minutes:"),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
//Expanded(child: Container(),),
FlatButton(
padding: EdgeInsets.all(0.0),
child: Text("-", style: Theme.of(context).textTheme.title),
onPressed: () => _decLocationInterval(),
),
Text("$_locationInterval", style: Theme.of(context).textTheme.title),
FlatButton(
padding: EdgeInsets.all(0.0),
child: Text("+", style: Theme.of(context).textTheme.title),
onPressed: () => _incLocationInterval(),
),
],
)
]
);
Text("Location tracking", style: Theme.of(context).textTheme.title),
Container(height: Sizes.rowPadding),
Row(
children: <Widget>[
Text("Enable"),
Switch(
value: _locationTrackingEnabled,
onChanged: _wait ? null : (value) {
setState(() {
_locationTrackingEnabled = value;
_wait = true;
});
_switchLocationTrackingState(value);
},
),
],
),
Container(height: Sizes.rowPadding),
Row(
children: <Widget>[
Text("Use foreground service"),
Switch(
value: _useForegroundService,
onChanged: _locationTrackingEnabled ? null : (value) {
setState(() {
_useForegroundService = value;
if (!_useForegroundService && _locationInterval.inMinutes < 15) {
_locationInterval = Duration(minutes: 15);
} else if (_useForegroundService) {
_showNotification = true;
}
});
},
),
],
),
Container(height: Sizes.rowPadding),
/*Text("Accuracy:", style: Theme.of(context).textTheme.body2),
Container(height: Sizes.rowPadding),
DropdownButton<int>(
value: _accuracy,
iconSize: 30.0,
isExpanded: true,
disabledHint: Text(locationAccuracy[_accuracy]),
items: locationAccuracy.keys.map((value) {
return new DropdownMenuItem<int>(
value: value,
child: Text('${locationAccuracy[value]}'),
);
}).toList(),
onChanged: _locationTrackingEnabled ? null : (val) {
setState(() {
_accuracy = val;
});
},
),
Container(height: Sizes.rowPadding),*/
Text("Update interval"),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
//Expanded(child: Container(),),
FlatButton(
padding: EdgeInsets.all(0.0),
child: Text("-", style: Theme.of(context).textTheme.headline4),
onPressed: _locationTrackingEnabled ? null : () => _decLocationInterval(),
),
Expanded(
child: Text(_formatInterval(),
textAlign: TextAlign.center,
style: _locationTrackingEnabled ? Theme.of(context).textTheme.title.copyWith(color: HAClientTheme().getDisabledStateColor(context)) : Theme.of(context).textTheme.title),
),
FlatButton(
padding: EdgeInsets.all(0.0),
child: Text("+", style: Theme.of(context).textTheme.headline4),
onPressed: _locationTrackingEnabled ? null : () => _incLocationInterval(),
),
],
),
Container(height: Sizes.rowPadding),
Row(
children: <Widget>[
Text("Show notification"),
Switch(
value: _showNotification,
onChanged: (_locationTrackingEnabled || _useForegroundService) ? null : (value) {
setState(() {
_showNotification = value;
});
},
),
],
),
Container(height: Sizes.rowPadding),
_getNotes()
]
);
}
@override
void dispose() {
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
super.dispose();
}
}

View File

@ -13,6 +13,7 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
AppTheme _currentTheme;
bool _scrollBadges = false;
DisplayMode _displayMode;
@override
void initState() {
@ -25,7 +26,8 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
await prefs.reload();
SharedPreferences.getInstance().then((prefs) {
setState(() {
_currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme.index];
_currentTheme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
_displayMode = DisplayMode.values[prefs.getInt('display-mode') ?? DisplayMode.normal.index];
_scrollBadges = prefs.getBool('scroll-badges') ?? true;
});
});
@ -42,18 +44,34 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
});
}
Future _saveOther() async {
Future _saveBadgesSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
ConnectionManager().scrollBadges = _scrollBadges;
AppSettings().scrollBadges = _scrollBadges;
await prefs.setBool('scroll-badges', _scrollBadges);
}
Future _saveDisplayMode(DisplayMode mode) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
AppSettings().displayMode = mode;
await prefs.setInt('display-mode', mode.index);
if (mode == DisplayMode.fullscreen) {
SystemChrome.setEnabledSystemUIOverlays([]);
} else {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
}
}
Map appThemeName = {
AppTheme.defaultTheme: 'Default',
AppTheme.haTheme: 'Home Assistant theme',
AppTheme.darkTheme: 'Dark theme'
};
Map DisplayModeName = {
DisplayMode.normal: 'Normal',
DisplayMode.fullscreen: 'Fullscreen'
};
@override
Widget build(BuildContext context) {
return ListView(
@ -93,7 +111,28 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
setState(() {
_scrollBadges = val;
});
_saveOther();
_saveBadgesSettings();
},
),
Container(height: Sizes.doubleRowPadding),
Text("Display mode:", style: Theme.of(context).textTheme.body2),
Container(height: Sizes.rowPadding),
DropdownButton<DisplayMode>(
value: _displayMode,
iconSize: 30.0,
isExpanded: true,
style: Theme.of(context).textTheme.title,
items: DisplayMode.values.map((value) {
return new DropdownMenuItem<DisplayMode>(
value: value,
child: Text('${DisplayModeName[value]}'),
);
}).toList(),
onChanged: (DisplayMode val) {
setState(() {
_displayMode = val;
});
_saveDisplayMode(val);
},
),
]

View File

@ -15,7 +15,7 @@ class ProductPurchase extends StatelessWidget {
String buttonText = '';
String buttonTextInactive = '';
if (product.id.contains("year")) {
period += "/ year";
period += "once a year";
buttonText = "Subscribe";
buttonTextInactive = "Already";
priceColor = Colors.amber;

View File

@ -20,7 +20,7 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
Widget build(BuildContext context) {
return ListView(
children: [
LinkToWebConfig(name: "Home Assistant configuration", url: ConnectionManager().httpWebHost+"/config"),
LinkToWebConfig(name: "Home Assistant configuration", url: AppSettings().httpWebHost+"/config"),
],
);
}

View File

@ -31,14 +31,15 @@ class Panel {
if (componentName.startsWith('haclient')) {
Navigator.of(context).pushNamed(urlPath);
} else if (componentName == 'lovelace') {
HomeAssistant().lovelaceDashboardUrl = this.urlPath;
HomeAssistant().currentDashboardPath = this.urlPath;
Logger.d('Switching to dashboard: ${this.urlPath}');
HomeAssistant().autoUi = false;
SharedPreferences.getInstance().then((prefs) {
prefs.setString('lovelace_dashboard_url', this.urlPath);
eventBus.fire(ReloadUIEvent());
});
} else {
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}");
Launcher.launchAuthenticatedWebView(context: context, url: "${AppSettings().httpWebHost}/$urlPath", title: "Back to app");
}
}

View File

@ -73,11 +73,9 @@ class TokenLoginPopup extends Popup {
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));
});
Hive.box(AppSettings.DEFAULT_HIVE_BOX).put(AppSettings.AUTH_TOKEN_KEY, newValue.trim());
Navigator.of(context).pop();
eventBus.fire(SettingsChangedEvent(true));
},
decoration: InputDecoration(
hintText: 'Please enter long-lived token',
@ -153,14 +151,14 @@ class RegisterAppPopup extends Popup {
Padding(
padding: EdgeInsets.all(20),
child: TextFormField(
initialValue: ConnectionManager().mobileAppDeviceName ?? MobileAppIntegrationManager.getDefaultDeviceName(),
initialValue: AppSettings().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;
AppSettings().webhookId = null;
AppSettings().mobileAppDeviceName = deviceName;
Navigator.of(context).pop();
MobileAppIntegrationManager.checkAppRegistration();
});

View File

@ -16,7 +16,7 @@ class HomeAssistantUI {
int viewCounter = 0;
Logger.d("--Views count: ${rawLovelaceConfig['views'].length}");
rawLovelaceConfig["views"].forEach((rawView){
Logger.d("----view id: ${rawView['id']}");
Logger.d("----view: ${rawView['path'] ?? viewCounter}");
HAView view = HAView(
count: viewCounter,
rawData: rawView
@ -30,8 +30,42 @@ class HomeAssistantUI {
}
Map _generateLovelaceConfig() {
Map result = {};
result['title'] = 'Home';
Map result = {
'title': 'Home'
};
List<Entity> left = HomeAssistant().entities.getByDomains(
excludeDomains: ['sensor','binary_sensor', 'device_tracker', 'person', 'sun']
);
List<Map> cards = [];
Map<String, Map> cardsByDomains = {};
left.forEach((Entity entity) {
if (entity is GroupEntity) {
cards.add({
'type': CardType.ENTITIES,
'title': entity.displayName,
'entities': entity.childEntities.map((e) => e.entityId)
});
} else if (entity is MediaPlayerEntity) {
cards.add({
'type': CardType.MEDIA_CONTROL,
'entity': entity.entityId
});
} else if (entity is AlarmControlPanelEntity) {
cards.add({
'type': CardType.ALARM_PANEL,
'entity': entity.entityId
});
} else if (cardsByDomains.containsKey(entity.domain)) {
cardsByDomains[entity.domain]['entities'].add(entity.entityId);
} else {
cardsByDomains[entity.domain] = {
'type': 'entities',
'entities': [entity.entityId],
'title': entity.domain
};
}
});
cards.addAll(cardsByDomains.values);
result['views'] = [
{
'icon': 'mdi:home',
@ -40,14 +74,7 @@ class HomeAssistantUI {
).map(
(en) => en.entityId
).toList(),
'cards': [{
'type': 'entities',
'entities': HomeAssistant().entities.getByDomains(
excludeDomains: ['sensor','binary_sensor', 'device_tracker', 'person', 'sun']
).map(
(en) => en.entityId
).toList()
}]
'cards': cards
}
];
return result;

View File

@ -0,0 +1,28 @@
part of '../main.dart';
class RandomColorGenerator {
static const colorsList = [
Colors.green,
Colors.purple,
Colors.indigo,
Colors.red,
Colors.orange,
Colors.cyan
];
int _index = 0;
Color getCurrent() {
return colorsList[_index];
}
Color getNext() {
if (_index < colorsList.length - 1) {
_index += 1;
} else {
_index = 1;
}
return getCurrent();
}
}

View File

@ -45,7 +45,7 @@ class Launcher {
if (viewState.type == standaloneWebview.WebViewState.startLoad) {
Logger.d("[WebView] Injecting external auth JS");
rootBundle.loadString('assets/js/externalAuth.js').then((js){
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token));
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", AppSettings().longLivedToken));
});
}
});

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ class HAView {
Entity linkedEntity;
String name;
String id;
String path;
String iconName;
final int count;
bool isPanel;
@ -15,6 +16,7 @@ class HAView {
name = rawData['title'];
iconName = rawData['icon'];
isPanel = rawData['panel'] ?? false;
path = rawData['path'] ?? '$count';
if (rawData['badges'] != null && !isPanel) {
badges = CardData.parse({
@ -24,7 +26,13 @@ class HAView {
}
(rawData['cards'] ?? []).forEach((rawCardData) {
cards.add(CardData.parse(rawCardData));
if (rawCardData != null) {
if (rawCardData is Map) {
cards.add(CardData.parse(rawCardData));
} else if (rawCardData is List && rawCardData.length == 1) {
cards.add(CardData.parse(rawCardData[0]));
}
}
});
}
@ -66,7 +74,7 @@ class HAView {
Widget build(BuildContext context) {
return ViewWidget(
view: this,
view: this
);
}
}
}

View File

@ -10,15 +10,12 @@ class ViewWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
Widget cardsContainer;
Widget badgesContainer;
if (this.view.isPanel) {
return FractionallySizedBox(
widthFactor: 1,
heightFactor: 1,
child: _buildPanelChild(context),
);
cardsContainer = _buildPanelChild(context);
badgesContainer = Container(width: 0, height: 0);
} else {
Widget cardsContainer;
Widget badgesContainer;
if (this.view.badges != null && this.view.badges is BadgesData) {
badgesContainer = this.view.badges.buildCardWidget();
} else {
@ -50,17 +47,17 @@ class ViewWidget extends StatelessWidget {
} else {
cardsContainer = Container();
}
return SingleChildScrollView(
padding: EdgeInsets.all(0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
badgesContainer,
cardsContainer
],
),
);
}
return SingleChildScrollView(
padding: EdgeInsets.all(0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
badgesContainer,
cardsContainer
],
),
);
}
Widget _buildPanelChild(BuildContext context) {

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 1.1.0+1100
version: 1.3.0+1309
environment:
@ -14,25 +14,22 @@ dependencies:
shared_preferences: ^0.5.6+1
path_provider: ^1.6.5
event_bus: ^1.1.1
cached_network_image: ^2.0.0
cached_network_image: ^2.3.0-beta
url_launcher: ^5.4.1
date_format: ^1.0.8
charts_flutter: ^0.8.1
flutter_markdown: ^0.3.3
in_app_purchase: ^0.3.0+3
in_app_purchase: ^0.3.4
flutter_custom_tabs: ^0.6.0
flutter_webview_plugin: ^0.3.10+1
webview_flutter: ^0.3.19+7
firebase_messaging: ^6.0.13
flutter_secure_storage: ^3.3.3
device_info: ^0.4.1+4
flutter_local_notifications: ^1.1.6
geolocator: ^5.3.1
workmanager: ^0.2.2
battery: ^1.0.0
hive: ^1.4.1+1
hive_flutter: ^0.3.0+2
device_info: ^0.4.2+4
firebase_crashlytics: ^0.1.3+3
syncfusion_flutter_core: ^18.1.48
syncfusion_flutter_gauges: ^18.1.48
syncfusion_flutter_core: ^18.2.44
syncfusion_flutter_gauges: ^18.2.44
flutter_map: ^0.10.1+1
dev_dependencies:
@ -51,4 +48,4 @@ flutter:
fonts:
- family: "Material Design Icons"
fonts:
- asset: fonts/materialdesignicons-webfont-4.5.95.ttf
- asset: fonts/materialdesignicons-webfont-5.3.45.ttf

File diff suppressed because one or more lines are too long