Compare commits

..

1 Commits

Author SHA1 Message Date
c844e21e76 WIP: Location tracking using foreground service 2020-05-12 22:18:41 +00:00
101 changed files with 5818 additions and 8163 deletions

View File

@ -1,24 +1,18 @@
# HA Client # HA Client
| # IMPORTANT! |
|:-:|
| As you may know official Home Assistant android app continue to grow and improve. Today is the day when it has so much more and better functionality that I see no any reason not to move from HA Client to official Home Assistant app. You need to make the same as it has more device sensors, better location tracking, widgets etc.
| **I'm stopping HA Client development as now the only advantage it has over the official app is native UI that is very far from completeness.** |
| **Login with Home Assistant credentials will be finally and irrevocably broken on Fabruary 1st of 2021 with the shut down of https://ha-client.app/.** |
| I need to say a HUGE THANK YOU all for a grate support you all made for HA Client. I really appreciate each and every issue reported to GitHub, each and every message from each of you and each donation made to support development. Now it is time to cancel subscription if any and migrate your HA setup to an official Home Assistant app. You can find it in [Google Play](https://play.google.com/store/apps/details?id=io.homeassistant.companion.android). |
## Native Android client for Home Assistant ## Native Android client for Home Assistant
### With actionable notifications, location tracking and Lovelace UI support ### With notifications and Lovelace UI support
Visit [ha-client.app](http://ha-client.app/) for more info. Visit [ha-client.app](http://ha-client.app/) for more info.
Download the app from [Google Play](https://play.google.com/store/apps/details?id=com.keyboardcrumbs.haclient) Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android) Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/estevez-dev/ha_client) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
#### Last release build status #### 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
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build) [![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
#### Pre-release CI build
#### Projects used [![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
- [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,10 +79,6 @@ flutter {
dependencies { dependencies {
implementation 'com.google.firebase:firebase-analytics:17.2.2' 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' testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

View File

@ -1,13 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <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" <uses-feature android:name="android.hardware.touchscreen"
android:required="false" /> android:required="false" />
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!--
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
-->
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
@ -17,6 +21,7 @@
additional functionality it is fine to subclass or reimplement additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. --> FlutterApplication and put your custom class here. -->
<application <application
android:name="io.flutter.app.FlutterApplication"
android:label="HA Client" android:label="HA Client"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
@ -43,45 +48,27 @@
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" /> android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<service <receiver android:name="rekab.app.background_locator.LocatorBroadcastReceiver"
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:enabled="true"
android:exported="false" /> android:exported="true"/>
<service <service android:name="rekab.app.background_locator.LocatorService"
android:name=".LocationRequestService" android:permission="android.permission.BIND_JOB_SERVICE"
android:enabled="true" android:exported="true"/>
android:exported="false" /> <service android:name="rekab.app.background_locator.IsolateHolderService"
<receiver android:name=".NotificationActionReceiver" android:exported="true"> android:permission="android.permission.FOREGROUND_SERVICE"
<intent-filter> android:exported="true"/>
<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 <service
android:name="io.flutter.plugins.androidalarmmanager.AlarmService" android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE" android:permission="android.permission.BIND_JOB_SERVICE"
@ -93,8 +80,9 @@
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver" android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
android:enabled="false"> android:enabled="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"></action>
</intent-filter> </intent-filter>
</receiver> </receiver>
-->
</application> </application>
</manifest> </manifest>

View File

@ -1,146 +0,0 @@
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

@ -1,154 +0,0 @@
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

@ -1,112 +0,0 @@
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

@ -1,147 +0,0 @@
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,184 +1,15 @@
package com.keyboardcrumbs.hassclient; package com.keyboardcrumbs.hassclient;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.work.WorkManager;
import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant; 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 { 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 @Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(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

@ -1,165 +0,0 @@
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

@ -1,54 +0,0 @@
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

@ -1,58 +0,0 @@
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

@ -1,15 +0,0 @@
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

@ -1,220 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 B

View File

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

Binary file not shown.

View File

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 108 108" width="108pt" height="108pt"><defs><clipPath id="_clipPath_5ibFn1qvppfzobeihHVrZ6swuOsurOBe"><rect width="108" height="108"/></clipPath></defs><g clip-path="url(#_clipPath_5ibFn1qvppfzobeihHVrZ6swuOsurOBe)"><rect width="108" height="108" style="fill:rgb(112,154,193)" fill-opacity="0"/><g><path d=" M 70.506 38.389 L 108 72.466 L 108 108 L 77 108 L 35.066 72.466 L 38.373 63.769 L 36.268 50.216 L 43.335 44.523 L 51.841 34.578 L 63.096 42.478 L 68.586 42.478 L 70.506 38.389 Z " fill="rgb(0,0,0)" fill-opacity="0.12"/><path d=" M 28.979 53.708 L 47.736 67.31 L 38.373 58.563 L 36.268 51.52 L 28.979 53.708 Z " fill="rgb(0,0,0)" fill-opacity="0.12"/></g><path d=" M 77.131 54.24 L 72.878 54.24 L 72.878 72.415 L 56.339 72.415 L 56.339 64.85 L 62.931 58.511 L 64.609 58.784 C 67.349 58.784 69.57 56.649 69.57 54.013 C 69.57 51.378 67.349 49.242 64.609 49.242 C 61.868 49.242 59.647 51.378 59.647 54.013 L 59.883 55.626 L 56.339 59.079 L 56.339 46.63 C 57.898 45.812 58.938 44.244 58.938 42.427 C 58.938 39.792 56.717 37.656 53.976 37.656 C 51.236 37.656 49.015 39.792 49.015 42.427 C 49.015 44.244 50.054 45.812 51.614 46.63 L 51.614 59.079 L 48.07 55.626 L 48.306 54.013 C 48.306 51.378 46.084 49.242 43.344 49.242 C 40.604 49.242 38.383 51.378 38.383 54.013 C 38.383 56.648 40.604 58.784 43.344 58.784 L 45.022 58.511 L 51.614 64.85 L 51.614 72.415 L 35.075 72.415 L 35.075 54.24 L 30.94 54.24 C 29.948 54.24 28.979 54.24 28.979 53.763 C 29.003 53.263 29.995 52.309 31.011 51.332 L 51.614 31.522 C 52.393 30.772 53.197 30 53.976 30 C 54.756 30 55.559 30.772 56.339 31.522 L 65.79 40.609 L 65.79 38.338 L 70.515 38.338 L 70.515 45.153 L 77.084 51.469 C 78.029 52.377 78.997 53.309 79.021 53.786 C 79.021 54.24 78.076 54.24 77.131 54.24 Z M 43.344 51.969 C 43.908 51.969 44.449 52.184 44.848 52.567 C 45.247 52.951 45.471 53.471 45.471 54.013 C 45.471 54.555 45.247 55.076 44.848 55.459 C 44.449 55.842 43.908 56.058 43.344 56.058 C 42.78 56.058 42.239 55.842 41.841 55.459 C 41.442 55.076 41.218 54.555 41.218 54.013 C 41.218 53.471 41.442 52.951 41.841 52.567 C 42.239 52.184 42.78 51.969 43.344 51.969 Z M 64.609 51.969 C 65.79 51.969 66.735 52.877 66.735 54.013 C 66.735 55.149 65.79 56.058 64.609 56.058 C 64.045 56.058 63.504 55.842 63.105 55.459 C 62.706 55.076 62.482 54.555 62.482 54.013 C 62.482 53.471 62.706 52.951 63.105 52.567 C 63.504 52.184 64.045 51.969 64.609 51.969 Z M 53.976 40.382 C 55.158 40.382 56.103 41.291 56.103 42.427 C 56.103 43.563 55.158 44.472 53.976 44.472 C 52.795 44.472 51.85 43.563 51.85 42.427 C 51.85 41.291 52.795 40.382 53.976 40.382 Z " fill="rgb(255,255,255)"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 512 512" width="512pt" height="512pt"><defs><clipPath id="_clipPath_wSm2gq78824Wp66cqyazQcLvYNPCHkOD"><rect width="512" height="512"/></clipPath></defs><g clip-path="url(#_clipPath_wSm2gq78824Wp66cqyazQcLvYNPCHkOD)"><rect width="512" height="512" style="fill:rgb(112,154,193)" fill-opacity="0"/><g><path d=" M 383.477 134.924 L 672.91 397.984 L 672.91 672.285 L 433.606 672.285 L 109.895 397.984 L 135.43 330.847 L 119.174 226.225 L 173.731 182.275 L 239.391 105.507 L 326.273 166.491 L 368.658 166.491 L 383.477 134.924 Z " fill="rgb(0,0,0)" fill-opacity="0.12"/><path d=" M 62.91 253.182 L 207.701 358.179 L 135.43 290.655 L 119.174 236.29 L 62.91 253.182 Z " fill="rgb(0,0,0)" fill-opacity="0.12"/></g><path d=" M 434.558 257.702 L 401.728 257.702 L 401.728 398 L 274.056 398 L 274.056 339.601 L 324.943 290.672 L 337.892 292.776 C 359.049 292.776 376.194 276.291 376.194 255.948 C 376.194 235.605 359.049 219.12 337.892 219.12 C 316.739 219.12 299.591 235.608 299.591 255.948 L 301.415 268.399 L 274.056 295.056 L 274.056 198.952 C 286.094 192.638 294.119 180.538 294.119 166.508 C 294.119 146.168 276.971 129.68 255.818 129.68 C 234.664 129.68 217.516 146.168 217.516 166.508 C 217.516 180.538 225.541 192.638 237.579 198.952 L 237.579 295.056 L 210.221 268.399 L 212.045 255.948 C 212.045 235.608 194.896 219.12 173.743 219.12 C 152.59 219.12 135.442 235.608 135.442 255.948 C 135.442 276.288 152.59 292.776 173.743 292.776 L 186.693 290.672 L 237.579 339.601 L 237.579 398 L 109.907 398 L 109.907 257.702 L 77.99 257.702 C 70.329 257.702 62.851 257.702 62.851 254.019 C 63.034 250.161 70.694 242.795 78.537 235.254 L 237.579 82.329 C 243.598 76.542 249.799 70.579 255.818 70.579 C 261.836 70.579 268.038 76.542 274.056 82.329 L 347.011 152.478 L 347.011 134.941 L 383.489 134.941 L 383.489 187.553 L 434.193 236.306 C 441.488 243.321 448.966 250.511 449.149 254.194 C 449.149 257.702 441.853 257.702 434.558 257.702 Z M 173.743 240.164 C 178.097 240.164 182.272 241.827 185.35 244.787 C 188.429 247.747 190.158 251.762 190.158 255.948 C 190.158 260.134 188.429 264.149 185.35 267.109 C 182.272 270.069 178.097 271.732 173.743 271.732 C 169.39 271.732 165.214 270.069 162.136 267.109 C 159.058 264.149 157.328 260.134 157.328 255.948 C 157.328 251.762 159.058 247.747 162.136 244.787 C 165.214 241.827 169.39 240.164 173.743 240.164 Z M 337.892 240.164 C 347.011 240.164 354.307 247.179 354.307 255.948 C 354.307 264.717 347.011 271.732 337.892 271.732 C 333.539 271.732 329.363 270.069 326.285 267.109 C 323.207 264.149 321.477 260.134 321.477 255.948 C 321.477 251.762 323.207 247.747 326.285 244.787 C 329.363 241.827 333.539 240.164 337.892 240.164 Z M 255.818 150.724 C 264.937 150.724 272.233 157.739 272.233 166.508 C 272.233 175.276 264.937 182.291 255.818 182.291 C 246.698 182.291 239.403 175.276 239.403 166.508 C 239.403 157.739 246.698 150.724 255.818 150.724 Z " fill="rgb(255,255,255)"/></g></svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

View File

@ -1,105 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="33.279999"
height="33.279999"
viewBox="0 0 33.279999 33.279999"
id="svg3384"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="ha_client.svg"
inkscape:export-filename="/home/vyalovegor/Downloads/ha_client.png"
inkscape:export-xdpi="1476.92"
inkscape:export-ydpi="1476.92">
<metadata
id="metadata3392">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3390">
<mask
maskUnits="userSpaceOnUse"
id="mask4987">
<circle
style="fill:#03a9f4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.52243769px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="circle4989"
cx="13.801129"
cy="2.4147582"
r="16.640072"
clip-path="none" />
</mask>
</defs>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2132"
inkscape:window-height="1090"
id="namedview3388"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="11.313709"
inkscape:cx="-10.334868"
inkscape:cy="12.876365"
inkscape:window-x="0"
inkscape:window-y="55"
inkscape:window-maximized="1"
inkscape:current-layer="svg3384" />
<circle
style="fill:#03a9f4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.52243769px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path3394"
cx="16.640072"
cy="16.640072"
r="16.640072"
clip-path="none" />
<path
style="fill:#000000;fill-opacity:0.40119761;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 14.075572,-8.35118 5.9827712,1.0714435 4.4798722,13.280061 26.759769,26.035624 37.105621,5.4863769 c -23.030049,-13.8375569 0,0 -23.030049,-13.8375569 z"
id="path4198"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc"
clip-path="none"
mask="url(#mask4987)"
transform="translate(2.8389435,14.225313)" />
<rect
style="fill:#626262;fill-opacity:1"
id="rect935"
width="4.895638"
height="3.3191559"
x="13.754303"
y="23.066553" />
<rect
id="rect68"
width="9.3691645"
height="10.871766"
x="11.667261"
y="12.243573"
style="fill:#fdd835;fill-opacity:1" />
<path
id="path116"
d="M 16.296894,5.2595255 3.4710675,17.036739 H 7.3188157 V 27.505374 H 25.274973 V 17.036739 h 3.847749 m -11.543244,7.851477 h -2.565166 v -1.30858 h 2.565166 m 0.64129,-3.166762 v 1.858183 h -3.847747 v -1.858183 a 3.8477482,3.9257379 0 1 1 3.847747,0 z"
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1;stroke-width:1.29551578" />
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M21.8,13H20V21H13V17.67L15.79,14.88L16.5,15C17.66,15 18.6,14.06 18.6,12.9C18.6,11.74 17.66,10.8 16.5,10.8A2.1,2.1 0 0,0 14.4,12.9L14.5,13.61L13,15.13V9.65C13.66,9.29 14.1,8.6 14.1,7.8A2.1,2.1 0 0,0 12,5.7A2.1,2.1 0 0,0 9.9,7.8C9.9,8.6 10.34,9.29 11,9.65V15.13L9.5,13.61L9.6,12.9A2.1,2.1 0 0,0 7.5,10.8A2.1,2.1 0 0,0 5.4,12.9A2.1,2.1 0 0,0 7.5,15L8.21,14.88L11,17.67V21H4V13H2.25C1.83,13 1.42,13 1.42,12.79C1.43,12.57 1.85,12.15 2.28,11.72L11,3C11.33,2.67 11.67,2.33 12,2.33C12.33,2.33 12.67,2.67 13,3L17,7V6H19V9L21.78,11.78C22.18,12.18 22.59,12.59 22.6,12.8C22.6,13 22.2,13 21.8,13M7.5,12A0.9,0.9 0 0,1 8.4,12.9A0.9,0.9 0 0,1 7.5,13.8A0.9,0.9 0 0,1 6.6,12.9A0.9,0.9 0 0,1 7.5,12M16.5,12C17,12 17.4,12.4 17.4,12.9C17.4,13.4 17,13.8 16.5,13.8A0.9,0.9 0 0,1 15.6,12.9A0.9,0.9 0 0,1 16.5,12M12,6.9C12.5,6.9 12.9,7.3 12.9,7.8C12.9,8.3 12.5,8.7 12,8.7C11.5,8.7 11.1,8.3 11.1,7.8C11.1,7.3 11.5,6.9 12,6.9Z" /></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12 3L2 12H5V20H19V12H22M13 18H11V17H13M13.5 14.58V16H10.5V14.58A3 3 0 1 1 13.5 14.58Z" /></svg>

Before

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 661 KiB

View File

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

View File

@ -21,6 +21,7 @@ class CardData {
switch (rawData['type']) { switch (rawData['type']) {
case CardType.ENTITIES: case CardType.ENTITIES:
case CardType.HISTORY_GRAPH: case CardType.HISTORY_GRAPH:
case CardType.MAP:
case CardType.PICTURE_GLANCE: case CardType.PICTURE_GLANCE:
case CardType.SENSOR: case CardType.SENSOR:
case CardType.ENTITY: case CardType.ENTITY:
@ -34,22 +35,8 @@ class CardData {
case CardType.ALARM_PANEL: case CardType.ALARM_PANEL:
return AlarmPanelCardData(rawData); return AlarmPanelCardData(rawData);
break; break;
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.ENTITY_BUTTON:
case CardType.LIGHT:
case CardType.BUTTON: case CardType.BUTTON:
case CardType.PICTURE_ENTITY: case CardType.PICTURE_ENTITY:
return ButtonCardData(rawData); return ButtonCardData(rawData);
@ -92,12 +79,6 @@ class CardData {
return BadgesData(rawData); return BadgesData(rawData);
break; break;
default: default:
if (rawData.containsKey('entity')) {
rawData['entities'] = [rawData['entity']];
}
if (rawData.containsKey('entities') && rawData['entities'] is List) {
return EntitiesCardData(rawData);
}
return CardData(null); return CardData(null);
} }
} catch (error, stacktrace) { } catch (error, stacktrace) {
@ -111,11 +92,7 @@ class CardData {
type = rawData['type']; type = rawData['type'];
conditions = rawData['conditions'] ?? []; conditions = rawData['conditions'] ?? [];
showEmpty = rawData['show_empty'] ?? true; showEmpty = rawData['show_empty'] ?? true;
if (rawData.containsKey('state_filter') && rawData['state_filter'] is List) { stateFilter = rawData['state_filter'] ?? [];
stateFilter = rawData['state_filter'];
} else {
stateFilter = [];
}
} else { } else {
type = CardType.UNKNOWN; type = CardType.UNKNOWN;
conditions = []; conditions = [];
@ -218,7 +195,7 @@ class BadgesData extends CardData {
entities.add( entities.add(
EntityWrapper( EntityWrapper(
entity: HomeAssistant().entities.get(rawBadge['entity']), entity: HomeAssistant().entities.get(rawBadge['entity']),
overrideName: rawBadge["name"]?.toString(), overrideName: rawBadge["name"],
overrideIcon: rawBadge["icon"], overrideIcon: rawBadge["icon"],
) )
); );
@ -244,7 +221,7 @@ class BadgesData extends CardData {
entities.add( entities.add(
EntityWrapper( EntityWrapper(
entity: e, entity: e,
overrideName: rawEntity["name"]?.toString(), overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"], overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []), stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []),
uiAction: EntityUIAction(rawEntityData: rawEntity) uiAction: EntityUIAction(rawEntityData: rawEntity)
@ -269,7 +246,7 @@ class EntitiesCardData extends CardData {
EntitiesCardData(rawData) : super(rawData) { EntitiesCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
title = rawData['title']?.toString(); title = rawData['title'];
icon = rawData['icon'] is String ? rawData['icon'] : null; icon = rawData['icon'] is String ? rawData['icon'] : null;
stateColor = rawData['state_color'] ?? false; stateColor = rawData['state_color'] ?? false;
showHeaderToggle = rawData['show_header_toggle'] ?? false; showHeaderToggle = rawData['show_header_toggle'] ?? false;
@ -300,7 +277,7 @@ class EntitiesCardData extends CardData {
EntityWrapper( EntityWrapper(
entity: Entity.callService( entity: Entity.callService(
icon: rawEntity["icon"], icon: rawEntity["icon"],
name: rawEntity["name"]?.toString(), name: rawEntity["name"],
service: rawEntity["service"], service: rawEntity["service"],
actionName: rawEntity["action_name"] actionName: rawEntity["action_name"]
), ),
@ -319,7 +296,7 @@ class EntitiesCardData extends CardData {
entities.add(EntityWrapper( entities.add(EntityWrapper(
entity: Entity.weblink( entity: Entity.weblink(
icon: rawEntity["icon"], icon: rawEntity["icon"],
name: rawEntity["name"]?.toString(), name: rawEntity["name"],
url: rawEntity["url"] url: rawEntity["url"]
), ),
stateColor: rawEntity["state_color"] ?? stateColor, stateColor: rawEntity["state_color"] ?? stateColor,
@ -332,7 +309,7 @@ class EntitiesCardData extends CardData {
EntityWrapper( EntityWrapper(
entity: e, entity: e,
stateColor: rawEntity["state_color"] ?? stateColor, stateColor: rawEntity["state_color"] ?? stateColor,
overrideName: rawEntity["name"]?.toString(), overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"], overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [], stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity) uiAction: EntityUIAction(rawEntityData: rawEntity)
@ -359,7 +336,7 @@ class AlarmPanelCardData extends CardData {
AlarmPanelCardData(rawData) : super(rawData) { AlarmPanelCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
name = rawData['name']?.toString(); name = rawData['name'];
states = rawData['states']; states = rawData['states'];
//Parsing entity //Parsing entity
var entitiId = rawData["entity"]; var entitiId = rawData["entity"];
@ -379,45 +356,6 @@ 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 { class ButtonCardData extends CardData {
String name; String name;
@ -434,7 +372,7 @@ class ButtonCardData extends CardData {
ButtonCardData(rawData) : super(rawData) { ButtonCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
name = rawData['name']?.toString(); name = rawData['name'];
icon = rawData['icon'] is String ? rawData['icon'] : null; icon = rawData['icon'] is String ? rawData['icon'] : null;
showName = rawData['show_name'] ?? true; showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true; showIcon = rawData['show_icon'] ?? true;
@ -497,7 +435,7 @@ class GaugeCardData extends CardData {
GaugeCardData(rawData) : super(rawData) { GaugeCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
name = rawData['name']?.toString(); name = rawData['name'];
unit = rawData['unit']; unit = rawData['unit'];
if (rawData['min'] is int) { if (rawData['min'] is int) {
min = rawData['min'].toDouble(); min = rawData['min'].toDouble();
@ -549,7 +487,7 @@ class GlanceCardData extends CardData {
GlanceCardData(rawData) : super(rawData) { GlanceCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
title = rawData["title"]?.toString(); title = rawData["title"];
showName = rawData['show_name'] ?? true; showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true; showIcon = rawData['show_icon'] ?? true;
showState = rawData['show_state'] ?? true; showState = rawData['show_state'] ?? true;
@ -571,7 +509,7 @@ class GlanceCardData extends CardData {
EntityWrapper( EntityWrapper(
entity: e, entity: e,
stateColor: stateColor, stateColor: stateColor,
overrideName: rawEntity["name"]?.toString(), overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"], overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [], stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity) uiAction: EntityUIAction(rawEntityData: rawEntity)
@ -646,52 +584,6 @@ 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 { class MediaControlCardData extends CardData {
@override @override

View File

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

View File

@ -1,161 +0,0 @@
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
)
);
}
}

View File

@ -1,88 +0,0 @@
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container(height: 20); return Container();
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -7,9 +7,8 @@ class EntityIcon extends StatelessWidget {
final EdgeInsetsGeometry imagePadding; final EdgeInsetsGeometry imagePadding;
final double size; final double size;
final Color color; final Color color;
final bool showBadge;
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); const EntityIcon({Key key, this.color, 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) { int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
if (entityId == null) { if (entityId == null) {
@ -49,41 +48,21 @@ class EntityIcon extends StatelessWidget {
); );
} else { } else {
if (entityWrapper.entityPicture != null) { if (entityWrapper.entityPicture != null) {
iconWidget = ClipOval( iconWidget = Container(
child: CachedNetworkImage(
imageUrl: '${entityWrapper.entityPicture}',
width: size+12,
fit: BoxFit.cover,
height: size+12, height: size+12,
errorWidget: (context, str, dyn) { width: size+12,
return Padding( decoration: BoxDecoration(
padding: iconPadding ?? padding, shape: BoxShape.circle,
child: _buildIcon(entityWrapper, iconColor) image: DecorationImage(
); fit:BoxFit.cover,
}, image: CachedNetworkImageProvider(
"${entityWrapper.entityPicture}"
),
)
), ),
); );
isPicture = true; isPicture = true;
} else { } else {
iconWidget = _buildIcon(entityWrapper, iconColor);
}
}
EdgeInsetsGeometry computedPadding;
if (isPicture && imagePadding != null) {
computedPadding = imagePadding;
} else if (!isPicture && iconPadding != null) {
computedPadding = iconPadding;
} else {
computedPadding = padding;
}
return Padding(
padding: computedPadding,
child: iconWidget,
);
}
Widget _buildIcon(EntityWrapper entityWrapper, Color iconColor) {
Widget iconWidget;
String iconName = entityWrapper.icon; String iconName = entityWrapper.icon;
int iconCode = 0; int iconCode = 0;
if (iconName.length > 0) { if (iconName.length > 0) {
@ -92,7 +71,7 @@ class EntityIcon extends StatelessWidget {
iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId, iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId,
entityWrapper.entity.deviceClass, entityWrapper.entity.state); // entityWrapper.entity.deviceClass, entityWrapper.entity.state); //
} }
if (showBadge && entityWrapper.entity is LightEntity && if (entityWrapper.entity is LightEntity &&
(entityWrapper.entity as LightEntity).supportColor && (entityWrapper.entity as LightEntity).supportColor &&
(entityWrapper.entity as LightEntity).color != null && (entityWrapper.entity as LightEntity).color != null &&
(entityWrapper.entity as LightEntity).color.toColor() != Colors.white (entityWrapper.entity as LightEntity).color.toColor() != Colors.white
@ -133,6 +112,19 @@ class EntityIcon extends StatelessWidget {
color: iconColor, color: iconColor,
); );
} }
return iconWidget; }
}
EdgeInsetsGeometry computedPadding;
if (isPicture && imagePadding != null) {
computedPadding = imagePadding;
} else if (!isPicture && iconPadding != null) {
computedPadding = iconPadding;
} else {
computedPadding = padding;
}
return Padding(
padding: computedPadding,
child: iconWidget,
);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:async'; import 'dart:async';
import 'dart:isolate';
import 'dart:ui';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
@ -16,26 +18,30 @@ import 'package:http/http.dart' as http;
import 'package:charts_flutter/flutter.dart' as charts; import 'package:charts_flutter/flutter.dart' as charts;
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart'; import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:hive/hive.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:device_info/device_info.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/dynamic_multi_column_layout.dart';
import 'plugins/spoiler_card.dart'; import 'plugins/spoiler_card.dart';
//import 'package:workmanager/workmanager.dart' as workManager;
//import 'package:geolocator/geolocator.dart';
import 'package:background_locator/background_locator.dart';
import 'package:background_locator/location_dto.dart';
import 'package:background_locator/location_settings.dart';
import 'package:battery/battery.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview; import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter/webview_flutter.dart';
import 'package:syncfusion_flutter_core/core.dart'; import 'package:syncfusion_flutter_core/core.dart';
import 'package:syncfusion_flutter_gauges/gauges.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 'utils/logger.dart';
import '.secrets.dart'; import '.secrets.dart';
part 'const.dart'; part 'const.dart';
part 'utils/launcher.dart'; part 'utils/launcher.dart';
part 'utils/RandomColorGenerator.dart';
part 'entities/entity.class.dart'; part 'entities/entity.class.dart';
part 'entities/entity_wrapper.class.dart'; part 'entities/entity_wrapper.class.dart';
part 'entities/timer/timer_entity.class.dart'; part 'entities/timer/timer_entity.class.dart';
@ -48,7 +54,6 @@ part 'entities/date_time/date_time_entity.class.dart';
part 'entities/light/light_entity.class.dart'; part 'entities/light/light_entity.class.dart';
part 'entities/select/select_entity.class.dart'; part 'entities/select/select_entity.class.dart';
part 'entities/sun/sun_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/sensor/sensor_entity.class.dart';
part 'entities/slider/slider_entity.dart'; part 'entities/slider/slider_entity.dart';
part 'entities/media_player/media_player_entity.class.dart'; part 'entities/media_player/media_player_entity.class.dart';
@ -62,7 +67,6 @@ part 'entities/entity_model.widget.dart';
part 'entities/default_entity_container.widget.dart'; part 'entities/default_entity_container.widget.dart';
part 'entities/missed_entity.widget.dart'; part 'entities/missed_entity.widget.dart';
part 'cards/entity_button_card.dart'; part 'cards/entity_button_card.dart';
part 'cards/map_card.dart';
part 'pages/widgets/entity_attributes_list.dart'; part 'pages/widgets/entity_attributes_list.dart';
part 'entities/entity_icon.widget.dart'; part 'entities/entity_icon.widget.dart';
part 'entities/entity_name.widget.dart'; part 'entities/entity_name.widget.dart';
@ -102,6 +106,8 @@ part 'entities/vacuum/widgets/vacuum_controls.dart';
part 'entities/vacuum/widgets/vacuum_state_button.dart'; part 'entities/vacuum/widgets/vacuum_state_button.dart';
part 'entities/error_entity_widget.dart'; part 'entities/error_entity_widget.dart';
part 'pages/settings/connection_settings.part.dart'; part 'pages/settings/connection_settings.part.dart';
part 'pages/purchase.page.dart';
part 'pages/widgets/product_purchase.widget.dart';
part 'pages/widgets/page_loading_indicator.dart'; part 'pages/widgets/page_loading_indicator.dart';
part 'pages/widgets/bottom_info_bar.dart'; part 'pages/widgets/bottom_info_bar.dart';
part 'pages/widgets/page_loading_error.dart'; part 'pages/widgets/page_loading_error.dart';
@ -117,6 +123,7 @@ part 'pages/entity.page.dart';
part 'utils/mdi.class.dart'; part 'utils/mdi.class.dart';
part 'entity_collection.class.dart'; part 'entity_collection.class.dart';
part 'managers/auth_manager.class.dart'; part 'managers/auth_manager.class.dart';
part 'managers/location_manager.class.dart';
part 'managers/mobile_app_integration_manager.class.dart'; part 'managers/mobile_app_integration_manager.class.dart';
part 'managers/connection_manager.class.dart'; part 'managers/connection_manager.class.dart';
part 'managers/device_info_manager.class.dart'; part 'managers/device_info_manager.class.dart';
@ -140,7 +147,6 @@ part 'cards/horizontal_srack_card.dart';
part 'cards/markdown_card.dart'; part 'cards/markdown_card.dart';
part 'cards/media_control_card.dart'; part 'cards/media_control_card.dart';
part 'cards/unsupported_card.dart'; part 'cards/unsupported_card.dart';
part 'cards/light_card.dart';
part 'cards/error_card.dart'; part 'cards/error_card.dart';
part 'cards/vertical_stack_card.dart'; part 'cards/vertical_stack_card.dart';
part 'cards/glance_card.dart'; part 'cards/glance_card.dart';
@ -152,12 +158,15 @@ part 'pages/whats_new.page.dart';
part 'pages/fullscreen.page.dart'; part 'pages/fullscreen.page.dart';
part 'popups.dart'; part 'popups.dart';
part 'cards/badges.dart'; part 'cards/badges.dart';
part 'managers/app_settings.dart';
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = 'HA Client'; const String appName = 'HA Client';
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0'); const appVersionNumber = '1.0.1';
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.3.1.md'; 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';
Future<void> _reportError(dynamic error, dynamic stackTrace) async { Future<void> _reportError(dynamic error, dynamic stackTrace) async {
// Print the exception to the console. // Print the exception to the console.
@ -182,17 +191,12 @@ void main() async {
}; };
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await AppSettings().loadStartupSettings(); SharedPreferences prefs = await SharedPreferences.getInstance();
await Hive.initFlutter(); AppTheme theme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
if (AppSettings().displayMode == DisplayMode.fullscreen) {
SystemChrome.setEnabledSystemUIOverlays([]);
} else {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
}
runZoned(() { runZoned(() {
runApp(new HAClientApp( runApp(new HAClientApp(
theme: AppSettings().appTheme, theme: theme,
)); ));
}, onError: (error, stack) { }, onError: (error, stack) {
_reportError(error, stack); _reportError(error, stack);
@ -211,20 +215,64 @@ class HAClientApp extends StatefulWidget {
} }
class _HAClientAppState extends State<HAClientApp> { class _HAClientAppState extends State<HAClientApp> {
StreamSubscription<List<PurchaseDetails>> _purchaseUpdateSubscription;
StreamSubscription _themeChangeSubscription; StreamSubscription _themeChangeSubscription;
AppTheme _currentTheme = AppTheme.defaultTheme; AppTheme _currentTheme = AppTheme.defaultTheme;
ReceivePort port = ReceivePort();
@override @override
void initState() { void initState() {
InAppPurchaseConnection.enablePendingPurchases();
final Stream purchaseUpdates =
InAppPurchaseConnection.instance.purchaseUpdatedStream;
_purchaseUpdateSubscription = purchaseUpdates.listen((purchases) {
_handlePurchaseUpdates(purchases);
});
_currentTheme = widget.theme; _currentTheme = widget.theme;
_themeChangeSubscription = eventBus.on<ChangeThemeEvent>().listen((event){ _themeChangeSubscription = eventBus.on<ChangeThemeEvent>().listen((event){
setState(() { setState(() {
_currentTheme = event.theme; _currentTheme = event.theme;
}); });
}); });
/*
workManager.Workmanager.initialize(
updateDeviceLocationIsolate,
isInDebugMode: false
);
*/
super.initState(); super.initState();
IsolateNameServer.registerPortWithName(port.sendPort, LocationManager.isolateName);
port.listen((dynamic data) {
// do something with data
});
initPlatformState();
} }
void _handlePurchaseUpdates(purchase) {
if (purchase is List<PurchaseDetails>) {
if (purchase[0].status == PurchaseStatus.purchased) {
eventBus.fire(ShowPopupEvent(
popup: Popup(
title: "Thanks a lot!",
body: "Thank you for supporting HA Client development!",
positiveText: "Ok"
)
));
} else {
Logger.d("Purchase change handler: ${purchase[0].status}");
}
} else {
Logger.e("Something wrong with purchase handling. Got: $purchase");
}
}
Future<void> initPlatformState() async {
await BackgroundLocator.initialize();
}
// This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new MaterialApp( return new MaterialApp(
@ -238,6 +286,7 @@ class _HAClientAppState extends State<HAClientApp> {
"/app-settings": (context) => AppSettingsPage(), "/app-settings": (context) => AppSettingsPage(),
"/connection-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.connectionSettings), "/connection-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.connectionSettings),
"/integration-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.integrationSettings), "/integration-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.integrationSettings),
"/putchase": (context) => PurchasePage(title: "Support app development"),
"/play-media": (context) => PlayMediaPage( "/play-media": (context) => PlayMediaPage(
mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}", mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}", mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
@ -256,7 +305,7 @@ class _HAClientAppState extends State<HAClientApp> {
"/quick-start": (context) => QuickStartPage(), "/quick-start": (context) => QuickStartPage(),
"/haclient_zha": (context) => ZhaPage(), "/haclient_zha": (context) => ZhaPage(),
"/auth": (context) => new standaloneWebview.WebviewScaffold( "/auth": (context) => new standaloneWebview.WebviewScaffold(
url: "${AppSettings().oauthUrl}", url: "${ConnectionManager().oauthUrl}",
appBar: new AppBar( appBar: new AppBar(
leading: IconButton( leading: IconButton(
icon: Icon(Icons.help), icon: Icon(Icons.help),
@ -284,6 +333,7 @@ class _HAClientAppState extends State<HAClientApp> {
@override @override
void dispose() { void dispose() {
_purchaseUpdateSubscription.cancel();
_themeChangeSubscription.cancel(); _themeChangeSubscription.cancel();
super.dispose(); super.dispose();
} }

View File

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

View File

@ -0,0 +1,328 @@
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 {
Logger.d('Starting location tracking');
BackgroundLocator.registerLocationUpdate(
locationCallback,
//optional
androidNotificationCallback: locationNotificationCallback,
settings: LocationSettings(
notificationTitle: "HA Client location tracking",
notificationMsg: "HA Client is updating your device location",
wakeLockTime: 20,
autoStop: false,
interval: 10
),
);
/*
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('Stopping location tracking');
IsolateNameServer.removePortNameMapping(isolateName);
BackgroundLocator.unRegisterLocationUpdate();
/*Logger.d("Canceling previous schedule if any...");
await workManager.Workmanager.cancelAll();*/
}
static const String isolateName = "HAClientLocatorIsolate";
static void locationCallback(LocationDto locationDto) async {
print('[Background location] Got location: $locationDto');
sendLocationData(locationDto);
final SendPort send = IsolateNameServer.lookupPortByName(isolateName);
send?.send(locationDto);
}
static Future<void> sendLocationData(LocationDto location) async {
print('[Background location] Loading settings...');
SharedPreferences prefs = await SharedPreferences.getInstance();
String domain = prefs.getString('hassio-domain');
String port = prefs.getString('hassio-port');
String webhookId = prefs.getString('app-webhook-id');
String httpWebHost =
"${prefs.getString('hassio-res-protocol')}://$domain:$port";
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
}
};
try {
if (location.longitude != null && location.latitude != null) {
data["data"]["gps"] = [location.latitude, location.longitude];
data["data"]["gps_accuracy"] = location.accuracy;
print('[Background location] Sending...');
try {
http.Response response = await http.post(
url,
headers: headers,
body: json.encode(data)
);
if (response.statusCode >= 200 && response.statusCode < 300) {
print('[Background location] Success!');
} else {
print('[Background location] Error sending data: ${response.statusCode}');
}
} catch(e) {
print('[Background location] Error sending data: $e');
}
} else {
print('[Background location] Error. Location is null');
}
} catch (e) {
print('[Background location] Error: $e');
}
}
}
static void locationNotificationCallback() {
print('[Background location] User clicked on the notification');
}
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,15 +2,17 @@ part of '../main.dart';
class MobileAppIntegrationManager { class MobileAppIntegrationManager {
static const INTEGRATION_VERSION = 3;
static final _appRegistrationData = { static final _appRegistrationData = {
"device_name": "", "device_name": "",
"app_version": "$appVersion", "app_version": "$appVersion",
"manufacturer": DeviceInfoManager().manufacturer ?? "unknown", "manufacturer": DeviceInfoManager().manufacturer,
"model": DeviceInfoManager().model ?? "unknown", "model": DeviceInfoManager().model,
"os_version": DeviceInfoManager().osVersion ?? "0", "os_version": DeviceInfoManager().osVersion,
"app_data": { "app_data": {
"push_token": "", "push_token": "",
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV3" "push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
} }
}; };
@ -21,29 +23,11 @@ class MobileAppIntegrationManager {
return '${HomeAssistant().userName}\'s ${DeviceInfoManager().model}'; return '${HomeAssistant().userName}\'s ${DeviceInfoManager().model}';
} }
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native'); static Future checkAppRegistration() {
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(); Completer completer = Completer();
_appRegistrationData["device_name"] = AppSettings().mobileAppDeviceName ?? getDefaultDeviceName(); _appRegistrationData["device_name"] = ConnectionManager().mobileAppDeviceName ?? getDefaultDeviceName();
(_appRegistrationData["app_data"] as Map)["push_token"] = "$fcmToken"; (_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
if (AppSettings().webhookId == null) { if (ConnectionManager().webhookId == null) {
Logger.d("Mobile app was not registered yet. Registering..."); Logger.d("Mobile app was not registered yet. Registering...");
var registrationData = Map.from(_appRegistrationData); var registrationData = Map.from(_appRegistrationData);
registrationData.addAll({ registrationData.addAll({
@ -52,7 +36,7 @@ class MobileAppIntegrationManager {
"os_name": DeviceInfoManager().osName, "os_name": DeviceInfoManager().osName,
"supports_encryption": false, "supports_encryption": false,
}); });
if (AppSettings().haVersion >= 104) { if (ConnectionManager().haVersion >= 104) {
registrationData.addAll({ registrationData.addAll({
"device_id": "${DeviceInfoManager().unicDeviceId}" "device_id": "${DeviceInfoManager().unicDeviceId}"
}); });
@ -62,13 +46,14 @@ class MobileAppIntegrationManager {
includeAuthHeader: true, includeAuthHeader: true,
data: json.encode(registrationData) data: json.encode(registrationData)
).then((response) { ).then((response) {
Logger.d("Processing registration response..."); Logger.d("Processing registration responce...");
var responseObject = json.decode(response); var responseObject = json.decode(response);
AppSettings().webhookId = responseObject["webhook_id"]; ConnectionManager().webhookId = responseObject["webhook_id"];
AppSettings().save({ ConnectionManager().appIntegrationVersion = INTEGRATION_VERSION;
'app-webhook-id': responseObject["webhook_id"] SharedPreferences.getInstance().then((prefs) {
}).then((_) { prefs.setString("app-webhook-id", responseObject["webhook_id"]);
_createNextAlarmSensor(true); prefs.setInt('app-integration-version', INTEGRATION_VERSION);
completer.complete(); completer.complete();
eventBus.fire(ShowPopupEvent( eventBus.fire(ShowPopupEvent(
popup: Popup( popup: Popup(
@ -91,6 +76,7 @@ class MobileAppIntegrationManager {
} }
_showError(); _showError();
}); });
return completer.future;
} else { } else {
Logger.d("App was previously registered. Checking..."); Logger.d("App was previously registered. Checking...");
var updateData = { var updateData = {
@ -98,7 +84,7 @@ class MobileAppIntegrationManager {
"data": _appRegistrationData "data": _appRegistrationData
}; };
ConnectionManager().sendHTTPPost( ConnectionManager().sendHTTPPost(
endPoint: "/api/webhook/${AppSettings().webhookId}", endPoint: "/api/webhook/${ConnectionManager().webhookId}",
includeAuthHeader: false, includeAuthHeader: false,
data: json.encode(updateData) data: json.encode(updateData)
).then((response) { ).then((response) {
@ -111,9 +97,13 @@ class MobileAppIntegrationManager {
if (registrationData == null || registrationData.isEmpty) { if (registrationData == null || registrationData.isEmpty) {
Logger.w("No registration data in response. MobileApp integration was removed or broken"); Logger.w("No registration data in response. MobileApp integration was removed or broken");
_askToRegisterApp(); _askToRegisterApp();
} else {
if (INTEGRATION_VERSION > ConnectionManager().appIntegrationVersion) {
Logger.d('App registration needs to be updated');
_askToRemoveAndRegisterApp();
} else { } else {
Logger.d('App registration works fine'); Logger.d('App registration works fine');
_createNextAlarmSensor(false); }
} }
completer.complete(); completer.complete();
}).catchError((e) { }).catchError((e) {
@ -129,44 +119,8 @@ class MobileAppIntegrationManager {
} }
completer.complete(); 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() { static void _showError() {
@ -174,11 +128,28 @@ class MobileAppIntegrationManager {
popup: Popup( popup: Popup(
title: "App integration is not working properly", 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.", 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 issue", positiveText: "Report to GitHub",
negativeText: "Close", negativeText: "Report to Discord",
onPositive: () { onPositive: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose"); 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");
},
) )
)); ));
} }

View File

@ -11,18 +11,45 @@ class StartupUserMessagesManager {
StartupUserMessagesManager._internal(); StartupUserMessagesManager._internal();
bool _supportAppDevelopmentMessageShown;
bool _whatsNewMessageShown; bool _whatsNewMessageShown;
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
static final _whatsNewMessageKey = "user-msg-whats-new-url"; static final _whatsNewMessageKey = "user-msg-whats-new-url";
void checkMessagesToShow() async { void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload(); await prefs.reload();
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
_whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl; _whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl;
if (!_whatsNewMessageShown) { if (!_whatsNewMessageShown) {
_showWhatsNewMessage(); _showWhatsNewMessage();
} else if (!_supportAppDevelopmentMessageShown) {
_showSupportAppDevelopmentMessage();
} }
} }
void _showSupportAppDevelopmentMessage() {
eventBus.fire(ShowPopupEvent(
popup: Popup(
title: "Hi!",
body: "As you may have noticed this app contains no ads. Also all app features are available for you for free. I'm not planning to change this in nearest future, but still you can support this application development materially. There is one-time payment available as well as several subscription options. Thanks.",
positiveText: "Show options",
negativeText: "Cancel",
onPositive: () {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_supportAppDevelopmentMessageKey, true);
eventBus.fire(ShowPageEvent(path: "/putchase"));
});
},
onNegative: () {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_supportAppDevelopmentMessageKey, true);
});
}
)
));
}
void _showWhatsNewMessage() { void _showWhatsNewMessage() {
SharedPreferences.getInstance().then((prefs) { SharedPreferences.getInstance().then((prefs) {
prefs.setString(_whatsNewMessageKey, whatsNewUrl); prefs.setString(_whatsNewMessageKey, whatsNewUrl);

View File

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

View File

@ -33,8 +33,35 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); 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(); _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) { _settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
Logger.d("Settings change event: reconnect=${event.reconnect}"); Logger.d("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) { if (event.reconnect) {
@ -46,18 +73,36 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_fullLoad(); _fullLoad();
} }
Future onSelectNotification(String payload) async {
if (payload != null) {
Logger.d('Notification clicked: ' + payload);
}
}
Future _showNotification({String title, String text}) async {
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service',
importance: Importance.Max, priority: Priority.High);
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
var platformChannelSpecifics = new NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
title ?? appName,
text,
platformChannelSpecifics
);
}
void _fullLoad() { void _fullLoad() {
_bottomInfoBarController.showInfoBottomBar(progress: true,); _bottomInfoBarController.showInfoBottomBar(progress: true,);
Logger.d('[loading] fullLoad');
_subscribe().then((_) { _subscribe().then((_) {
Logger.d('[loading] subscribed');
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){ ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
Logger.d('[loading] COnnection manager initialized');
SharedPreferences.getInstance().then((prefs) { SharedPreferences.getInstance().then((prefs) {
HomeAssistant().currentDashboardPath = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD; HomeAssistant().lovelaceDashboardUrl = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
_fetchData(useCache: true); _fetchData(useCache: true);
LocationManager();
StartupUserMessagesManager().checkMessagesToShow(); StartupUserMessagesManager().checkMessagesToShow();
MobileAppIntegrationManager.checkAppRegistration();
}); });
}, onError: (e) { }, onError: (e) {
if (e is HACNotSetUpException) { if (e is HACNotSetUpException) {
@ -109,7 +154,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
} }
Future _subscribe() async { Future _subscribe() {
Completer completer = Completer();
if (_stateSubscription == null) { if (_stateSubscription == null) {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) { if (event.needToRebuildUI) {
@ -190,15 +237,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}); });
} }
/*_firebaseMessaging.getToken().then((String token) { _firebaseMessaging.getToken().then((String token) {
HomeAssistant().fcmToken = token; HomeAssistant().fcmToken = token;
completer.complete(); completer.complete();
});*/ });
return completer.future;
} }
void _showOAuth() { void _showOAuth() {
_preventAppRefresh = true; _preventAppRefresh = true;
Navigator.of(context).pushNamed("/auth", arguments: {"url": AppSettings().oauthUrl}); Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
} }
_setErrorState(HACException e) { _setErrorState(HACException e) {
@ -300,7 +348,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
title: Text("Report an issue"), title: Text("Report an issue"),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose"); Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
},
),
Divider(),
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:food")),
title: Text("Support app development"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/putchase');
}, },
), ),
Divider(), Divider(),
@ -392,37 +449,82 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget _buildScaffoldBody(bool empty) { Widget _buildScaffoldBody(bool empty) {
List<Entity> activePlayers = []; List<PopupMenuItem<String>> serviceMenuItems = [];
List<Entity> activeLights = []; List<PopupMenuItem<String>> mediaMenuItems = [];
Color mediaMenuIconColor;
Color lightMenuIconColor;
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0; int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
if (_previousViewCount != currentViewCount) { 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); _viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount; _previousViewCount = currentViewCount;
} }
if (AppSettings().isAuthenticated) { serviceMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
));
if (ConnectionManager().isAuthenticated) {
_showLoginButton = false; _showLoginButton = false;
serviceMenuItems.add(
PopupMenuItem<String>(
child: new Text("Logout"),
value: "logout",
));
} }
Widget mediaMenuIcon;
int playersCount = 0;
if (!empty && !HomeAssistant().entities.isEmpty) { if (!empty && !HomeAssistant().entities.isEmpty) {
activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]); List<Entity> activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
activeLights = HomeAssistant().entities.getByDomains(includeDomains: ["light"], stateFiler: [EntityState.on]); 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()
);
} }
mediaMenuItems.addAll([
if (activePlayers.isNotEmpty) { PopupMenuItem<String>(
mediaMenuIconColor = Theme.of(context).accentColor; 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
)
),
),
),
)
],
);
} else { } else {
mediaMenuIconColor = Theme.of(context).primaryIconTheme.color; mediaMenuIcon = Icon(MaterialDesignIcons.getIconDataFromIconName(
} "mdi:television"), color: Colors.white,);
if (activeLights.isNotEmpty) {
lightMenuIconColor = Theme.of(context).accentColor;
} else {
lightMenuIconColor = Theme.of(context).primaryIconTheme.color;
} }
Widget mainScrollBody; Widget mainScrollBody;
if (empty) { if (empty) {
@ -473,43 +575,46 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
SliverAppBar( SliverAppBar(
floating: true, floating: true,
pinned: true, pinned: true,
snap: false, primary: true,
primary: AppSettings().displayMode == DisplayMode.normal,
title: Text(HomeAssistant().locationName ?? ""), title: Text(HomeAssistant().locationName ?? ""),
actions: <Widget>[ actions: <Widget>[
PopupMenuButton( IconButton(
child: Padding( icon: mediaMenuIcon,
padding: EdgeInsets.symmetric(horizontal: 15), onPressed: () {
child: Icon(MaterialDesignIcons.getIconDataFromIconName( showMenu(
"mdi:dots-vertical"), color: Theme.of(context).primaryIconTheme.color) 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);
}
});
}
), ),
onSelected: (String val) { 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") { if (val == "reload") {
_quickLoad(); _quickLoad();
} else if (val == "logout") { } else if (val == "logout") {
HomeAssistant().logout().then((_) { HomeAssistant().logout().then((_) {
_quickLoad(); _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; )
},
),
], ],
leading: IconButton( leading: IconButton(
icon: Icon(Icons.menu), icon: Icon(Icons.menu),
@ -517,99 +622,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_scaffoldKey.currentState.openDrawer(); _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( bottom: empty ? null : TabBar(
controller: _viewsTabController, controller: _viewsTabController,
tabs: buildUIViewTabs(), tabs: buildUIViewTabs(),
@ -653,7 +665,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
Hive.close();
//final flutterWebviewPlugin = new FlutterWebviewPlugin(); //final flutterWebviewPlugin = new FlutterWebviewPlugin();
//flutterWebviewPlugin.dispose(); //flutterWebviewPlugin.dispose();
_viewsTabController?.dispose(); _viewsTabController?.dispose();

View File

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

View File

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

View File

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

View File

@ -11,18 +11,9 @@ class IntegrationSettingsPage extends StatefulWidget {
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> { class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native'); int _locationInterval = LocationManager().defaultUpdateIntervalMinutes;
/*static final locationAccuracy = {
100: "High",
102: "Balanced"
};*/
Duration _locationInterval;
bool _locationTrackingEnabled = false; bool _locationTrackingEnabled = false;
bool _wait = false; bool _wait = false;
bool _showNotification = true;
//int _accuracy = 100;
bool _useForegroundService = false;
@override @override
void initState() { void initState() {
@ -36,127 +27,43 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
await prefs.reload(); await prefs.reload();
SharedPreferences.getInstance().then((prefs) { SharedPreferences.getInstance().then((prefs) {
setState(() { setState(() {
//_accuracy = prefs.getInt("location-updates-priority") ?? 100; _locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
_locationTrackingEnabled = (prefs.getInt("location-updates-state") ?? 0) > 0; _locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
_showNotification = prefs.getBool("location-updates-show-notification") ?? true; if (_locationInterval % 5 != 0) {
_useForegroundService = prefs.getBool("foreground-location-tracking") ?? false; _locationInterval = 5 * (_locationInterval ~/ 5);
_locationInterval = Duration(milliseconds: prefs.getInt("location-updates-interval") ?? }
900000);
}); });
}); });
} }
void _incLocationInterval() { void _incLocationInterval() {
if (_locationInterval.inSeconds < 60) { if (_locationInterval < 720) {
setState(() { setState(() {
_locationInterval = _locationInterval + Duration(seconds: 5); _locationInterval = _locationInterval + 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() { void _decLocationInterval() {
if ((_useForegroundService && _locationInterval.inSeconds > 5) || (!_useForegroundService && _locationInterval.inMinutes > 15)) { if (_locationInterval > 5) {
setState(() { setState(() {
if (_locationInterval.inSeconds <= 60) { _locationInterval = _locationInterval - 5;
_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 { _switchLocationTrackingState(bool state) async {
/*
if (state) { if (state) {
try { await LocationManager().updateDeviceLocation();
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(() { setState(() {
_wait = false; _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 more accurate and stable tracking', 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( return ListView(
@ -164,10 +71,21 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
children: <Widget>[ children: <Widget>[
Text("Location tracking", style: Theme.of(context).textTheme.title), Text("Location tracking", style: Theme.of(context).textTheme.title),
Container(height: Sizes.rowPadding), 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( Row(
children: <Widget>[ children: <Widget>[
Text("Enable"), Text("Enable device location tracking"),
Switch( Switch(
value: _locationTrackingEnabled, value: _locationTrackingEnabled,
onChanged: _wait ? null : (value) { onChanged: _wait ? null : (value) {
@ -180,47 +98,8 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
), ),
], ],
), ),
Container(height: Sizes.rowPadding), Container(height: Sizes.rowPadding,),
Row( Text("Location update interval in minutes:"),
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( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
@ -228,43 +107,24 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
//Expanded(child: Container(),), //Expanded(child: Container(),),
FlatButton( FlatButton(
padding: EdgeInsets.all(0.0), padding: EdgeInsets.all(0.0),
child: Text("-", style: Theme.of(context).textTheme.headline4), child: Text("-", style: Theme.of(context).textTheme.title),
onPressed: _locationTrackingEnabled ? null : () => _decLocationInterval(), onPressed: () => _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),
), ),
Text("$_locationInterval", style: Theme.of(context).textTheme.title),
FlatButton( FlatButton(
padding: EdgeInsets.all(0.0), padding: EdgeInsets.all(0.0),
child: Text("+", style: Theme.of(context).textTheme.headline4), child: Text("+", style: Theme.of(context).textTheme.title),
onPressed: _locationTrackingEnabled ? null : () => _incLocationInterval(), onPressed: () => _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 @override
void dispose() { void dispose() {
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
super.dispose(); super.dispose();
} }
} }

View File

@ -13,7 +13,6 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
AppTheme _currentTheme; AppTheme _currentTheme;
bool _scrollBadges = false; bool _scrollBadges = false;
DisplayMode _displayMode;
@override @override
void initState() { void initState() {
@ -26,8 +25,7 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
await prefs.reload(); await prefs.reload();
SharedPreferences.getInstance().then((prefs) { SharedPreferences.getInstance().then((prefs) {
setState(() { setState(() {
_currentTheme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.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; _scrollBadges = prefs.getBool('scroll-badges') ?? true;
}); });
}); });
@ -44,34 +42,18 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
}); });
} }
Future _saveBadgesSettings() async { Future _saveOther() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
AppSettings().scrollBadges = _scrollBadges; ConnectionManager().scrollBadges = _scrollBadges;
await prefs.setBool('scroll-badges', _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 = { Map appThemeName = {
AppTheme.defaultTheme: 'Default', AppTheme.defaultTheme: 'Default',
AppTheme.haTheme: 'Home Assistant theme', AppTheme.haTheme: 'Home Assistant theme',
AppTheme.darkTheme: 'Dark theme' AppTheme.darkTheme: 'Dark theme'
}; };
Map DisplayModeName = {
DisplayMode.normal: 'Normal',
DisplayMode.fullscreen: 'Fullscreen'
};
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( return ListView(
@ -111,28 +93,7 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
setState(() { setState(() {
_scrollBadges = val; _scrollBadges = val;
}); });
_saveBadgesSettings(); _saveOther();
},
),
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

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

View File

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

View File

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

View File

@ -73,9 +73,11 @@ class TokenLoginPopup extends Popup {
padding: EdgeInsets.all(20), padding: EdgeInsets.all(20),
child: TextFormField( child: TextFormField(
onSaved: (newValue) { onSaved: (newValue) {
Hive.box(AppSettings.DEFAULT_HIVE_BOX).put(AppSettings.AUTH_TOKEN_KEY, newValue.trim()); final storage = new FlutterSecureStorage();
storage.write(key: "hacl_llt", value: newValue.trim()).then((_) {
Navigator.of(context).pop(); Navigator.of(context).pop();
eventBus.fire(SettingsChangedEvent(true)); eventBus.fire(SettingsChangedEvent(true));
});
}, },
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Please enter long-lived token', hintText: 'Please enter long-lived token',
@ -151,14 +153,14 @@ class RegisterAppPopup extends Popup {
Padding( Padding(
padding: EdgeInsets.all(20), padding: EdgeInsets.all(20),
child: TextFormField( child: TextFormField(
initialValue: AppSettings().mobileAppDeviceName ?? MobileAppIntegrationManager.getDefaultDeviceName(), initialValue: ConnectionManager().mobileAppDeviceName ?? MobileAppIntegrationManager.getDefaultDeviceName(),
onSaved: (newValue) { onSaved: (newValue) {
String deviceName = newValue?.trim(); String deviceName = newValue?.trim();
SharedPreferences.getInstance().then((prefs) { SharedPreferences.getInstance().then((prefs) {
prefs.remove("app-webhook-id"); prefs.remove("app-webhook-id");
prefs.setString('app-integration-device-name', deviceName); prefs.setString('app-integration-device-name', deviceName);
AppSettings().webhookId = null; ConnectionManager().webhookId = null;
AppSettings().mobileAppDeviceName = deviceName; ConnectionManager().mobileAppDeviceName = deviceName;
Navigator.of(context).pop(); Navigator.of(context).pop();
MobileAppIntegrationManager.checkAppRegistration(); MobileAppIntegrationManager.checkAppRegistration();
}); });

View File

@ -16,7 +16,7 @@ class HomeAssistantUI {
int viewCounter = 0; int viewCounter = 0;
Logger.d("--Views count: ${rawLovelaceConfig['views'].length}"); Logger.d("--Views count: ${rawLovelaceConfig['views'].length}");
rawLovelaceConfig["views"].forEach((rawView){ rawLovelaceConfig["views"].forEach((rawView){
Logger.d("----view: ${rawView['path'] ?? viewCounter}"); Logger.d("----view id: ${rawView['id']}");
HAView view = HAView( HAView view = HAView(
count: viewCounter, count: viewCounter,
rawData: rawView rawData: rawView
@ -30,42 +30,8 @@ class HomeAssistantUI {
} }
Map _generateLovelaceConfig() { Map _generateLovelaceConfig() {
Map result = { Map result = {};
'title': 'Home' 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'] = [ result['views'] = [
{ {
'icon': 'mdi:home', 'icon': 'mdi:home',
@ -74,7 +40,14 @@ class HomeAssistantUI {
).map( ).map(
(en) => en.entityId (en) => en.entityId
).toList(), ).toList(),
'cards': cards 'cards': [{
'type': 'entities',
'entities': HomeAssistant().entities.getByDomains(
excludeDomains: ['sensor','binary_sensor', 'device_tracker', 'person', 'sun']
).map(
(en) => en.entityId
).toList()
}]
} }
]; ];
return result; return result;

View File

@ -1,28 +0,0 @@
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) { if (viewState.type == standaloneWebview.WebViewState.startLoad) {
Logger.d("[WebView] Injecting external auth JS"); Logger.d("[WebView] Injecting external auth JS");
rootBundle.loadString('assets/js/externalAuth.js').then((js){ rootBundle.loadString('assets/js/externalAuth.js').then((js){
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", AppSettings().longLivedToken)); flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token));
}); });
} }
}); });

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -10,12 +10,15 @@ class ViewWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (this.view.isPanel) {
return FractionallySizedBox(
widthFactor: 1,
heightFactor: 1,
child: _buildPanelChild(context),
);
} else {
Widget cardsContainer; Widget cardsContainer;
Widget badgesContainer; Widget badgesContainer;
if (this.view.isPanel) {
cardsContainer = _buildPanelChild(context);
badgesContainer = Container(width: 0, height: 0);
} else {
if (this.view.badges != null && this.view.badges is BadgesData) { if (this.view.badges != null && this.view.badges is BadgesData) {
badgesContainer = this.view.badges.buildCardWidget(); badgesContainer = this.view.badges.buildCardWidget();
} else { } else {
@ -47,7 +50,6 @@ class ViewWidget extends StatelessWidget {
} else { } else {
cardsContainer = Container(); cardsContainer = Container();
} }
}
return SingleChildScrollView( return SingleChildScrollView(
padding: EdgeInsets.all(0), padding: EdgeInsets.all(0),
child: Column( child: Column(
@ -59,6 +61,7 @@ class ViewWidget extends StatelessWidget {
), ),
); );
} }
}
Widget _buildPanelChild(BuildContext context) { Widget _buildPanelChild(BuildContext context) {
if (this.view.cards != null && this.view.cards.isNotEmpty) { if (this.view.cards != null && this.view.cards.isNotEmpty) {

View File

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

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