Compare commits
	
		
			37 Commits
		
	
	
		
			rc/1.1.0-b
			...
			1.2.0-beta
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | f84728b948 | ||
|  | 26a62d341e | ||
|  | 772bddeb9a | ||
|  | 5b55940ccf | ||
|  | 7683d18e81 | ||
|  | d09afc37b5 | ||
|  | 1c686402d0 | ||
|  | 5f4a3fbdfc | ||
|  | 312ed99e9f | ||
|  | 25e6d51c17 | ||
|  | b501574bab | ||
|  | 53b31d8e90 | ||
|  | 6d80420a9b | ||
|  | e977054139 | ||
|  | 6367d38524 | ||
|  | f9b2d7d84c | ||
|  | 44c28ad106 | ||
|  | fec3c525e1 | ||
|  | b1bbed6d80 | ||
|  | 13878cfc51 | ||
|  | be49180205 | ||
|  | c4a0b16553 | ||
|  | caacd5e9f4 | ||
|  | 5fa28abb6c | ||
|  | e0a28c0b59 | ||
|  | 096e714a04 | ||
|  | 78893ea01f | ||
|  | 90efb29be5 | ||
|  | fca323c56b | ||
|  | e5fe6af5f3 | ||
|  | f0090d522d | ||
|  | edbfd8359b | ||
|  | 2702bb254a | ||
|  | ca7b6ed550 | ||
|  | fb00b5d9ff | ||
|  | 7ffba397ce | ||
|  | 1080076e3b | 
| @@ -13,6 +13,6 @@ Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant commun | |||||||
| #### Last release build status | #### Last release build status | ||||||
| [](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build) | [](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build) | ||||||
|  |  | ||||||
| #### Special thanks to | #### Projects used | ||||||
| - [Crewski](https://github.com/Crewski) for his [HANotify](https://github.com/Crewski/HANotify) | - [HANotify](https://github.com/Crewski/HANotify) by [Crewski](https://github.com/Crewski) | ||||||
| - [Home Assistant](https://github.com/home-assistant) for some support and [Home Assistant](https://www.home-assistant.io/) | - [hassalarm](https://github.com/Johboh/hassalarm) by [Johboh](https://github.com/Johboh) distributed under [MIT License](https://github.com/Johboh/hassalarm/blob/master/LICENSE) | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| <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" /> | ||||||
| @@ -61,7 +62,12 @@ | |||||||
|                 <action android:name="android.intent.action.INPUT_METHOD_CHANGED" /> |                 <action android:name="android.intent.action.INPUT_METHOD_CHANGED" /> | ||||||
|             </intent-filter> |             </intent-filter> | ||||||
|         </receiver> |         </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> | ||||||
|         <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" | ||||||
| @@ -73,7 +79,7 @@ | |||||||
|             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> |                 <action android:name="android.intent.action.BOOT_COMPLETED"/> | ||||||
|             </intent-filter> |             </intent-filter> | ||||||
|         </receiver> |         </receiver> | ||||||
|     </application> |     </application> | ||||||
|   | |||||||
| @@ -16,6 +16,8 @@ import io.flutter.plugin.common.MethodChannel; | |||||||
|  |  | ||||||
| import com.google.android.gms.tasks.OnCompleteListener; | import com.google.android.gms.tasks.OnCompleteListener; | ||||||
| import com.google.android.gms.tasks.Task; | import com.google.android.gms.tasks.Task; | ||||||
|  | import com.google.android.gms.common.GoogleApiAvailability; | ||||||
|  | import com.google.android.gms.common.ConnectionResult; | ||||||
| import com.google.firebase.iid.FirebaseInstanceId; | import com.google.firebase.iid.FirebaseInstanceId; | ||||||
| import com.google.firebase.iid.InstanceIdResult; | import com.google.firebase.iid.InstanceIdResult; | ||||||
| import com.google.firebase.messaging.FirebaseMessaging; | import com.google.firebase.messaging.FirebaseMessaging; | ||||||
| @@ -31,28 +33,36 @@ public class MainActivity extends FlutterActivity { | |||||||
|             new MethodChannel.MethodCallHandler() { |             new MethodChannel.MethodCallHandler() { | ||||||
|                 @Override |                 @Override | ||||||
|                 public void onMethodCall(MethodCall call, MethodChannel.Result result) { |                 public void onMethodCall(MethodCall call, MethodChannel.Result result) { | ||||||
|  |                     Context context = getActivity(); | ||||||
|                     if (call.method.equals("getFCMToken")) { |                     if (call.method.equals("getFCMToken")) { | ||||||
|  |                         if (checkPlayServices()) { | ||||||
|                             FirebaseInstanceId.getInstance().getInstanceId() |                             FirebaseInstanceId.getInstance().getInstanceId() | ||||||
|                             .addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() { |                             .addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() { | ||||||
|                                 @Override |                                 @Override | ||||||
|                                 public void onComplete(@NonNull Task<InstanceIdResult> task) { |                                 public void onComplete(@NonNull Task<InstanceIdResult> task) { | ||||||
|                                     if (task.isSuccessful()) { |                                     if (task.isSuccessful()) { | ||||||
|                                         Context context = getActivity(); |  | ||||||
|                                         String token = task.getResult().getToken(); |                                         String token = task.getResult().getToken(); | ||||||
|                                         UpdateTokenTask updateTokenTask = new UpdateTokenTask(context); |                                         UpdateTokenTask updateTokenTask = new UpdateTokenTask(context); | ||||||
|                                         updateTokenTask.execute(token); |                                         updateTokenTask.execute(token); | ||||||
|                                         result.success(token); |                                         result.success(token); | ||||||
|                                     } else { |                                     } else { | ||||||
|                                         result.error("fcm_error", task.getException().getMessage(), task.getException()); |                                         result.error("fcm_error", task.getException().getMessage(), null); | ||||||
|                                     } |                                     } | ||||||
|                                 } |                                 } | ||||||
|                             }); |                             }); | ||||||
|  |                         } else { | ||||||
|  |                             result.error("google_play_service_error", "Google Play Services unavailable", null); | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private boolean checkPlayServices() { | ||||||
|  |         return (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |     protected void onCreate(Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|   | |||||||
| @@ -145,7 +145,7 @@ public class MessagingService extends FirebaseMessagingService { | |||||||
|             connection.connect(); |             connection.connect(); | ||||||
|             InputStream input = connection.getInputStream(); |             InputStream input = connection.getInputStream(); | ||||||
|             return BitmapFactory.decodeStream(input); |             return BitmapFactory.decodeStream(input); | ||||||
|         } catch (IOException e) { |         } catch (Exception e) { | ||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -0,0 +1,51 @@ | |||||||
|  | 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.ExistingWorkPolicy; | ||||||
|  | import androidx.work.NetworkType; | ||||||
|  | import androidx.work.OneTimeWorkRequest; | ||||||
|  | import androidx.work.WorkManager; | ||||||
|  | import androidx.work.WorkRequest; | ||||||
|  |  | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | public class NextAlarmBroadcastReceiver extends BroadcastReceiver { | ||||||
|  |  | ||||||
|  |     private static final String TAG = "NextAlarmReceiver"; | ||||||
|  |  | ||||||
|  |     @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(); | ||||||
|  |  | ||||||
|  |         OneTimeWorkRequest uploadWorkRequest = | ||||||
|  |                 new OneTimeWorkRequest.Builder(UpdateNextAlarmWorker.class) | ||||||
|  |                         .setBackoffCriteria( | ||||||
|  |                                 BackoffPolicy.EXPONENTIAL, | ||||||
|  |                                 10, | ||||||
|  |                                 TimeUnit.SECONDS) | ||||||
|  |                         .setConstraints(constraints) | ||||||
|  |                         .build(); | ||||||
|  |  | ||||||
|  |         WorkManager | ||||||
|  |                 .getInstance(context) | ||||||
|  |                 .enqueueUniqueWork("NextAlarmUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| package com.keyboardcrumbs.hassclient; | package com.keyboardcrumbs.hassclient; | ||||||
|  |  | ||||||
|  | import android.app.AlarmManager; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.content.BroadcastReceiver; | import android.content.BroadcastReceiver; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| @@ -15,10 +15,14 @@ import android.content.SharedPreferences; | |||||||
|  |  | ||||||
| public class NotificationActionReceiver extends BroadcastReceiver { | public class NotificationActionReceiver extends BroadcastReceiver { | ||||||
|  |  | ||||||
|     private static final String TAG = "NotificationActionReceiver"; |     private static final String TAG = "NotificationAction"; | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onReceive(Context context, Intent intent) { |     public void onReceive(Context context, Intent intent) { | ||||||
|  |         if (intent == null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         String rawActionData = intent.getStringExtra("actionData"); |         String rawActionData = intent.getStringExtra("actionData"); | ||||||
|         if (intent.hasExtra("tag")) { |         if (intent.hasExtra("tag")) { | ||||||
|             String notificationTag = intent.getStringExtra("tag"); |             String notificationTag = intent.getStringExtra("tag"); | ||||||
|   | |||||||
| @@ -0,0 +1,119 @@ | |||||||
|  | package com.keyboardcrumbs.hassclient; | ||||||
|  |  | ||||||
|  | import android.app.AlarmManager; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | 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.text.SimpleDateFormat; | ||||||
|  | import java.util.Calendar; | ||||||
|  | import java.util.Locale; | ||||||
|  |  | ||||||
|  | public class UpdateNextAlarmWorker extends Worker { | ||||||
|  |  | ||||||
|  |     private Context currentContext; | ||||||
|  |     private static final String TAG = "NextAlarmWorker"; | ||||||
|  |     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 UpdateNextAlarmWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { | ||||||
|  |         super(context, workerParams); | ||||||
|  |         currentContext = context; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public Result doWork() { | ||||||
|  |         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(); | ||||||
|  |  | ||||||
|  |         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; | ||||||
|  |                 JSONObject dataToSend = new JSONObject(); | ||||||
|  |                 if (URLUtil.isValidUrl(requestUrl)) { | ||||||
|  |                     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); | ||||||
|  |                         sensorData.put("state", DATE_TIME_FORMAT.format(calendar.getTime())); | ||||||
|  |                         sensorAttrs.put("date", DATE_FORMAT.format(calendar.getTime())); | ||||||
|  |                         sensorAttrs.put("time", TIME_FORMAT.format(calendar.getTime())); | ||||||
|  |                         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); | ||||||
|  |  | ||||||
|  |                     String stringRequest = dataToSend.toString(); | ||||||
|  |                     try { | ||||||
|  |                         URL url = new URL(requestUrl); | ||||||
|  |                         HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); | ||||||
|  |                         urlConnection.setRequestMethod("POST"); | ||||||
|  |                         urlConnection.setRequestProperty("Content-Type", "application/json"); | ||||||
|  |                         urlConnection.setDoOutput(true); | ||||||
|  |                         byte[] outputBytes = stringRequest.getBytes("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 setting next alarm", e); | ||||||
|  |                 return Result.failure(); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             Log.w(TAG, "Webhook id not found"); | ||||||
|  |             return Result.failure(); | ||||||
|  |         } | ||||||
|  |         return Result.success(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,6 +1,4 @@ | |||||||
| org.gradle.jvmargs=-Xmx2g | org.gradle.jvmargs=-Xmx512m | ||||||
| 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 | ||||||
|   | |||||||
| @@ -38,6 +38,15 @@ class CardData { | |||||||
|           case CardType.LIGHT: |           case CardType.LIGHT: | ||||||
|             return LightCardData(rawData); |             return LightCardData(rawData); | ||||||
|             break; |             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.ENTITY_BUTTON: |           case CardType.ENTITY_BUTTON: | ||||||
|           case CardType.BUTTON: |           case CardType.BUTTON: | ||||||
|           case CardType.PICTURE_ENTITY: |           case CardType.PICTURE_ENTITY: | ||||||
| @@ -81,6 +90,12 @@ 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) { | ||||||
| @@ -94,7 +109,11 @@ class CardData { | |||||||
|       type = rawData['type']; |       type = rawData['type']; | ||||||
|       conditions = rawData['conditions'] ?? []; |       conditions = rawData['conditions'] ?? []; | ||||||
|       showEmpty = rawData['show_empty'] ?? true; |       showEmpty = rawData['show_empty'] ?? true; | ||||||
|       stateFilter = rawData['state_filter']  ?? []; |       if (rawData.containsKey('state_filter') && rawData['state_filter'] is List) { | ||||||
|  |         stateFilter = rawData['state_filter']; | ||||||
|  |       } else { | ||||||
|  |         stateFilter = []; | ||||||
|  |       } | ||||||
|     } else { |     } else { | ||||||
|       type = CardType.UNKNOWN; |       type = CardType.UNKNOWN; | ||||||
|       conditions = []; |       conditions = []; | ||||||
| @@ -365,8 +384,14 @@ class LightCardData extends CardData { | |||||||
|    |    | ||||||
|   @override |   @override | ||||||
|   Widget buildCardWidget() { |   Widget buildCardWidget() { | ||||||
|  |     if (this.entity != null && this.entity.entity is LightEntity) { | ||||||
|       return LightCard(card: this); |       return LightCard(card: this); | ||||||
|     } |     } | ||||||
|  |     return ErrorCard( | ||||||
|  |       errorText: 'Specify an entity from within the light domain.', | ||||||
|  |       showReportButton: false, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|    |    | ||||||
|   LightCardData(rawData) : super(rawData) { |   LightCardData(rawData) : super(rawData) { | ||||||
|     //Parsing card data |     //Parsing card data | ||||||
|   | |||||||
| @@ -2,12 +2,21 @@ 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}) : super(key: key); |   const ErrorCard({Key key, this.card, this.errorText, this.showReportButton: true}) : 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( | ||||||
| @@ -15,21 +24,25 @@ class ErrorCard extends StatelessWidget { | |||||||
|           crossAxisAlignment: CrossAxisAlignment.center, |           crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|           children: <Widget>[ |           children: <Widget>[ | ||||||
|             Text( |             Text( | ||||||
|               'There was an error rendering card: ${card.type}. Please copy card config to clipboard and report this issue. Thanks!', |               error, | ||||||
|               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) | ||||||
|           ], |           ], | ||||||
|         ), |         ), | ||||||
|       ) |       ) | ||||||
|   | |||||||
| @@ -7,6 +7,6 @@ class UnsupportedCard extends StatelessWidget { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Container(); |     return Container(height: 20); | ||||||
|   }   |   }   | ||||||
| } | } | ||||||
| @@ -4,12 +4,14 @@ 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.padding: const EdgeInsets.all(0)}) : super(key: key); |   const CardWrapper({Key key, this.child, this.color, this.padding: const EdgeInsets.all(0)}) : super(key: key); | ||||||
|  |  | ||||||
|   @override |   @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 | ||||||
|   | |||||||
| @@ -40,8 +40,8 @@ class CoverEntity extends Entity { | |||||||
|       CoverEntity.SUPPORT_SET_TILT_POSITION); |       CoverEntity.SUPPORT_SET_TILT_POSITION); | ||||||
|  |  | ||||||
|  |  | ||||||
|   double get currentPosition => _getDoubleAttributeValue('current_position'); |   double get currentPosition => _getDoubleAttributeValue('current_position') ?? 0; | ||||||
|   double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position'); |   double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position') ?? 0; | ||||||
|   bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0); |   bool get 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; | ||||||
|   | |||||||
| @@ -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) { | ||||||
|   | |||||||
| @@ -149,7 +149,7 @@ class EntityCollection { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool isExist(String entityId) { |   bool isExist(String entityId) { | ||||||
|     return _allEntities[entityId] != null; |     return _allEntities.containsKey(entityId); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) { |   List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) { | ||||||
|   | |||||||
| @@ -221,7 +221,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);   |        Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true);   | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     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 +261,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); |         Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true); | ||||||
|         panels.clear(); |         panels.clear(); | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|   | |||||||
| @@ -16,7 +16,8 @@ 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:flutter_secure_storage/flutter_secure_storage.dart'; | import 'package:hive/hive.dart'; | ||||||
|  | import 'package:hive_flutter/hive_flutter.dart'; | ||||||
| import 'package:device_info/device_info.dart'; | import 'package:device_info/device_info.dart'; | ||||||
| import 'package:in_app_purchase/in_app_purchase.dart'; | import 'package:in_app_purchase/in_app_purchase.dart'; | ||||||
| import 'plugins/dynamic_multi_column_layout.dart'; | import 'plugins/dynamic_multi_column_layout.dart'; | ||||||
| @@ -155,11 +156,9 @@ part 'cards/badges.dart'; | |||||||
| part 'managers/app_settings.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 String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0'); | ||||||
| const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.1.0-b2.md'; | const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.2.0.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. | ||||||
| @@ -184,7 +183,13 @@ void main() async { | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   WidgetsFlutterBinding.ensureInitialized(); |   WidgetsFlutterBinding.ensureInitialized(); | ||||||
|   AppSettings().loadAppTheme(); |   await AppSettings().loadStartupSettings(); | ||||||
|  |   await Hive.initFlutter(); | ||||||
|  |   if (AppSettings().displayMode == DisplayMode.fullscreen) { | ||||||
|  |     SystemChrome.setEnabledSystemUIOverlays([]); | ||||||
|  |   } else { | ||||||
|  |     SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   runZoned(() { |   runZoned(() { | ||||||
|       runApp(new HAClientApp( |       runApp(new HAClientApp( | ||||||
| @@ -242,6 +247,7 @@ class _HAClientAppState extends State<HAClientApp> { | |||||||
|             positiveText: "Ok" |             positiveText: "Ok" | ||||||
|           ) |           ) | ||||||
|         )); |         )); | ||||||
|  |         InAppPurchaseConnection.instance.completePurchase(purchase[0]); | ||||||
|       } else { |       } else { | ||||||
|         Logger.d("Purchase change handler: ${purchase[0].status}"); |         Logger.d("Purchase change handler: ${purchase[0].status}"); | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -1,7 +1,13 @@ | |||||||
| part of '../main.dart'; | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | enum DisplayMode {normal, fullscreen} | ||||||
|  |  | ||||||
| class AppSettings { | class AppSettings { | ||||||
|  |  | ||||||
|  |   static const DEFAULT_HIVE_BOX = 'defaultSettingsBox'; | ||||||
|  |  | ||||||
|  |   static const AUTH_TOKEN_KEY = 'llt'; | ||||||
|  |  | ||||||
|   static final AppSettings _instance = AppSettings._internal(); |   static final AppSettings _instance = AppSettings._internal(); | ||||||
|  |  | ||||||
|   factory AppSettings() { |   factory AppSettings() { | ||||||
| @@ -22,6 +28,8 @@ class AppSettings { | |||||||
|   String webhookId; |   String webhookId; | ||||||
|   double haVersion; |   double haVersion; | ||||||
|   bool scrollBadges; |   bool scrollBadges; | ||||||
|  |   bool nextAlarmSensorCreated = false; | ||||||
|  |   DisplayMode displayMode; | ||||||
|   AppTheme appTheme; |   AppTheme appTheme; | ||||||
|   final int defaultLocationUpdateIntervalMinutes = 20; |   final int defaultLocationUpdateIntervalMinutes = 20; | ||||||
|   Duration locationUpdateInterval; |   Duration locationUpdateInterval; | ||||||
| @@ -30,13 +38,15 @@ class AppSettings { | |||||||
|   bool get isAuthenticated => longLivedToken != null; |   bool get isAuthenticated => longLivedToken != null; | ||||||
|   bool get isTempAuthenticated => tempToken != null; |   bool get isTempAuthenticated => tempToken != null; | ||||||
|  |  | ||||||
|   loadAppTheme() async { |   loadStartupSettings() async { | ||||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); |     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||||
|     appTheme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index]; |     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 { |   Future load(bool full) async { | ||||||
|     if (full) { |     if (full) { | ||||||
|  |       await Hive.openBox(DEFAULT_HIVE_BOX); | ||||||
|       Logger.d('Loading settings...'); |       Logger.d('Loading settings...'); | ||||||
|       SharedPreferences prefs = await SharedPreferences.getInstance(); |       SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||||
|       _domain = prefs.getString('hassio-domain'); |       _domain = prefs.getString('hassio-domain'); | ||||||
| @@ -52,18 +62,12 @@ class AppSettings { | |||||||
|       locationUpdateInterval = Duration(minutes: prefs.getInt("location-interval") ?? |       locationUpdateInterval = Duration(minutes: prefs.getInt("location-interval") ?? | ||||||
|         defaultLocationUpdateIntervalMinutes); |         defaultLocationUpdateIntervalMinutes); | ||||||
|       locationTrackingEnabled = prefs.getBool("location-enabled") ?? false; |       locationTrackingEnabled = prefs.getBool("location-enabled") ?? false; | ||||||
|       Logger.d('Done. $_domain:$_port'); |       nextAlarmSensorCreated = prefs.getBool("next-alarm-sensor-created") ?? false; | ||||||
|       try { |       longLivedToken = Hive.box(DEFAULT_HIVE_BOX).get(AUTH_TOKEN_KEY); | ||||||
|         final storage = new FlutterSecureStorage(); |  | ||||||
|         longLivedToken = await storage.read(key: "hacl_llt"); |  | ||||||
|         Logger.d("Long-lived token read successful"); |  | ||||||
|       oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent( |       oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent( | ||||||
|           'https://ha-client.app')}&redirect_uri=${Uri |           'https://ha-client.app')}&redirect_uri=${Uri | ||||||
|           .encodeComponent( |           .encodeComponent( | ||||||
|           'https://ha-client.app/service/auth_callback.html')}"; |           'https://ha-client.app/service/auth_callback.html')}"; | ||||||
|       } catch (e, stacktrace) { |  | ||||||
|         Logger.e("Error reading secure storage: $e", stacktrace: stacktrace); |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -103,25 +107,13 @@ class AppSettings { | |||||||
|   Future clearTokens() async { |   Future clearTokens() async { | ||||||
|     longLivedToken = null; |     longLivedToken = null; | ||||||
|     tempToken = null; |     tempToken = null; | ||||||
|     try { |     Hive.box(DEFAULT_HIVE_BOX).delete(AUTH_TOKEN_KEY); | ||||||
|       final storage = new FlutterSecureStorage(); |  | ||||||
|       await storage.delete(key: "hacl_llt"); |  | ||||||
|     } catch(e, stacktrace) { |  | ||||||
|       Logger.e("Error clearing tokens: $e", stacktrace: stacktrace); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future saveLongLivedToken(token) async { |   void saveLongLivedToken(token) { | ||||||
|     longLivedToken = token; |     longLivedToken = token; | ||||||
|     tempToken = null; |     tempToken = null; | ||||||
|     try { |     Hive.box(DEFAULT_HIVE_BOX).put(AUTH_TOKEN_KEY, longLivedToken); | ||||||
|       final storage = new FlutterSecureStorage(); |  | ||||||
|       await storage.write(key: "hacl_llt", value: "$longLivedToken"); |  | ||||||
|       SharedPreferences prefs = await SharedPreferences.getInstance(); |  | ||||||
|       prefs.setBool("oauth-used", true); |  | ||||||
|     } catch(e, stacktrace) { |  | ||||||
|       Logger.e("Error saving long-lived token: $e", stacktrace: stacktrace); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool isNotConfigured() { |   bool isNotConfigured() { | ||||||
|   | |||||||
| @@ -254,6 +254,7 @@ class ConnectionManager { | |||||||
|     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); |       AppSettings().saveLongLivedToken(data); | ||||||
|  |       completer.complete(); | ||||||
|     }).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")])); | ||||||
|     }); |     }); | ||||||
|   | |||||||
| @@ -5,9 +5,9 @@ class MobileAppIntegrationManager { | |||||||
|   static final _appRegistrationData = { |   static final _appRegistrationData = { | ||||||
|     "device_name": "", |     "device_name": "", | ||||||
|     "app_version": "$appVersion", |     "app_version": "$appVersion", | ||||||
|     "manufacturer": DeviceInfoManager().manufacturer, |     "manufacturer": DeviceInfoManager().manufacturer ?? "unknown", | ||||||
|     "model": DeviceInfoManager().model, |     "model": DeviceInfoManager().model ?? "unknown", | ||||||
|     "os_version": DeviceInfoManager().osVersion, |     "os_version": DeviceInfoManager().osVersion ?? "0", | ||||||
|     "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/pushNotifyV3" | ||||||
| @@ -62,12 +62,13 @@ class MobileAppIntegrationManager { | |||||||
|           includeAuthHeader: true, |           includeAuthHeader: true, | ||||||
|           data: json.encode(registrationData) |           data: json.encode(registrationData) | ||||||
|       ).then((response) { |       ).then((response) { | ||||||
|         Logger.d("Processing registration responce..."); |         Logger.d("Processing registration response..."); | ||||||
|         var responseObject = json.decode(response); |         var responseObject = json.decode(response); | ||||||
|         AppSettings().webhookId = responseObject["webhook_id"]; |         AppSettings().webhookId = responseObject["webhook_id"]; | ||||||
|         AppSettings().save({ |         AppSettings().save({ | ||||||
|           'app-webhook-id': responseObject["webhook_id"] |           'app-webhook-id': responseObject["webhook_id"] | ||||||
|         }).then((prefs) { |         }).then((_) { | ||||||
|  |           _createNextAlarmSensor(true); | ||||||
|           completer.complete(); |           completer.complete(); | ||||||
|           eventBus.fire(ShowPopupEvent( |           eventBus.fire(ShowPopupEvent( | ||||||
|             popup: Popup( |             popup: Popup( | ||||||
| @@ -112,6 +113,7 @@ class MobileAppIntegrationManager { | |||||||
|           _askToRegisterApp(); |           _askToRegisterApp(); | ||||||
|         } else { |         } else { | ||||||
|           Logger.d('App registration works fine'); |           Logger.d('App registration works fine'); | ||||||
|  |           _createNextAlarmSensor(false); | ||||||
|         } |         } | ||||||
|         completer.complete(); |         completer.complete(); | ||||||
|       }).catchError((e) { |       }).catchError((e) { | ||||||
| @@ -131,6 +133,42 @@ class MobileAppIntegrationManager { | |||||||
|     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() { | ||||||
|     eventBus.fire(ShowPopupEvent( |     eventBus.fire(ShowPopupEvent( | ||||||
|       popup: Popup( |       popup: Popup( | ||||||
| @@ -139,7 +177,7 @@ class MobileAppIntegrationManager { | |||||||
|         positiveText: "Report issue", |         positiveText: "Report issue", | ||||||
|         negativeText: "Close", |         negativeText: "Close", | ||||||
|         onPositive: () { |         onPositive: () { | ||||||
|           Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new"); |           Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose"); | ||||||
|         } |         } | ||||||
|       ) |       ) | ||||||
|     )); |     )); | ||||||
|   | |||||||
| @@ -1,18 +1,37 @@ | |||||||
| part of '../main.dart'; | part of '../main.dart'; | ||||||
|  |  | ||||||
| class FullScreenPage extends StatelessWidget { | class FullScreenPage extends StatefulWidget { | ||||||
|  |  | ||||||
|   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.child, |         child: this.widget.child, | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void dispose() { | ||||||
|  |     SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values); | ||||||
|  |     super.dispose(); | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -301,7 +301,7 @@ 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"); |           Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose"); | ||||||
|         }, |         }, | ||||||
|       ), |       ), | ||||||
|       Divider(), |       Divider(), | ||||||
| @@ -484,7 +484,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|               floating: true, |               floating: true, | ||||||
|               pinned: true, |               pinned: true, | ||||||
|               snap: false, |               snap: false, | ||||||
|               primary: true, |               primary: AppSettings().displayMode == DisplayMode.normal, | ||||||
|               title: Text(HomeAssistant().locationName ?? ""), |               title: Text(HomeAssistant().locationName ?? ""), | ||||||
|               actions: <Widget>[ |               actions: <Widget>[ | ||||||
|                 PopupMenuButton( |                 PopupMenuButton( | ||||||
| @@ -493,6 +493,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|                     child: Icon(MaterialDesignIcons.getIconDataFromIconName( |                     child: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||||
|                         "mdi:dots-vertical"), color: Theme.of(context).primaryIconTheme.color) |                         "mdi:dots-vertical"), color: Theme.of(context).primaryIconTheme.color) | ||||||
|                   ), |                   ), | ||||||
|  |                   onSelected: (String val) { | ||||||
|  |                     if (val == "reload") { | ||||||
|  |                       _quickLoad(); | ||||||
|  |                     } else if (val == "logout")  { | ||||||
|  |                       HomeAssistant().logout().then((_) { | ||||||
|  |                         _quickLoad(); | ||||||
|  |                       }); | ||||||
|  |                     } | ||||||
|  |                   }, | ||||||
|                   itemBuilder: (BuildContext context) { |                   itemBuilder: (BuildContext context) { | ||||||
|                     List<PopupMenuEntry<String>> result = [ |                     List<PopupMenuEntry<String>> result = [ | ||||||
|                       PopupMenuItem<String>( |                       PopupMenuItem<String>( | ||||||
| @@ -654,6 +663,7 @@ 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(); | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ class _PurchasePageState extends State<PurchasePage> { | |||||||
|     } else { |     } else { | ||||||
|       const Set<String> _kIds = {'one_time_support','just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'}; |       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); |       final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds); | ||||||
|       if (!response.notFoundIDs.isEmpty) { |       if (response.notFoundIDs.isNotEmpty) { | ||||||
|         Logger.d("Products not found: ${response.notFoundIDs}"); |         Logger.d("Products not found: ${response.notFoundIDs}"); | ||||||
|       } |       } | ||||||
|       _products = response.productDetails; |       _products = response.productDetails; | ||||||
| @@ -63,7 +63,18 @@ class _PurchasePageState extends State<PurchasePage> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   List<Widget> _buildProducts() { |   List<Widget> _buildProducts() { | ||||||
|     List<Widget> productWidgets = []; |     List<Widget> productWidgets = [ | ||||||
|  |       Card( | ||||||
|  |         child: Padding( | ||||||
|  |           padding: EdgeInsets.all(15), | ||||||
|  |           child: Text( | ||||||
|  |               'This will not unlock any additional functionality. This is only a donation to the HA Client open source project.', | ||||||
|  |               style: Theme.of(context).textTheme.headline5, | ||||||
|  |               textAlign: TextAlign.center, | ||||||
|  |           ) | ||||||
|  |         ) | ||||||
|  |       ) | ||||||
|  |     ]; | ||||||
|     for (ProductDetails product in _products) { |     for (ProductDetails product in _products) { | ||||||
|       productWidgets.add( |       productWidgets.add( | ||||||
|         ProductPurchase( |         ProductPurchase( | ||||||
| @@ -90,22 +101,6 @@ class _PurchasePageState extends State<PurchasePage> { | |||||||
|     } else { |     } else { | ||||||
|       body = _buildProducts(); |       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( |     return new Scaffold( | ||||||
|       appBar: new AppBar( |       appBar: new AppBar( | ||||||
|         leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){ |         leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){ | ||||||
|   | |||||||
| @@ -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, style: Theme.of(context).textTheme.subhead), |       title: Text(title), | ||||||
|       leading: Icon(icon), |       leading: Icon(icon), | ||||||
|       trailing: Icon(Icons.keyboard_arrow_right), |       trailing: Icon(Icons.keyboard_arrow_right), | ||||||
|       onTap: () { |       onTap: () { | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> { | |||||||
|  |  | ||||||
|   AppTheme _currentTheme; |   AppTheme _currentTheme; | ||||||
|   bool _scrollBadges = false; |   bool _scrollBadges = false; | ||||||
|  |   DisplayMode _displayMode; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
| @@ -25,7 +26,8 @@ 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; | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| @@ -42,18 +44,34 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future _saveOther() async { |   Future _saveBadgesSettings() async { | ||||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); |     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||||
|     AppSettings().scrollBadges = _scrollBadges; |     AppSettings().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( | ||||||
| @@ -93,7 +111,28 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> { | |||||||
|               setState(() { |               setState(() { | ||||||
|                 _scrollBadges = val; |                 _scrollBadges = val; | ||||||
|               }); |               }); | ||||||
|               _saveOther(); |               _saveBadgesSettings(); | ||||||
|  |             }, | ||||||
|  |           ), | ||||||
|  |           Container(height: Sizes.doubleRowPadding), | ||||||
|  |           Text("Display mode:", style: Theme.of(context).textTheme.body2), | ||||||
|  |           Container(height: Sizes.rowPadding), | ||||||
|  |           DropdownButton<DisplayMode>( | ||||||
|  |             value: _displayMode, | ||||||
|  |             iconSize: 30.0, | ||||||
|  |             isExpanded: true, | ||||||
|  |             style: Theme.of(context).textTheme.title, | ||||||
|  |             items: DisplayMode.values.map((value) { | ||||||
|  |               return new DropdownMenuItem<DisplayMode>( | ||||||
|  |                 value: value, | ||||||
|  |                 child: Text('${DisplayModeName[value]}'), | ||||||
|  |               ); | ||||||
|  |             }).toList(), | ||||||
|  |             onChanged: (DisplayMode val) { | ||||||
|  |               setState(() { | ||||||
|  |                 _displayMode = val; | ||||||
|  |               }); | ||||||
|  |               _saveDisplayMode(val); | ||||||
|             }, |             }, | ||||||
|           ), |           ), | ||||||
|         ] |         ] | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ class ProductPurchase extends StatelessWidget { | |||||||
|     String buttonText = ''; |     String buttonText = ''; | ||||||
|     String buttonTextInactive = ''; |     String buttonTextInactive = ''; | ||||||
|     if (product.id.contains("year")) { |     if (product.id.contains("year")) { | ||||||
|       period += "/ year"; |       period += "once a year"; | ||||||
|       buttonText = "Subscribe"; |       buttonText = "Subscribe"; | ||||||
|       buttonTextInactive = "Already"; |       buttonTextInactive = "Already"; | ||||||
|       priceColor = Colors.amber; |       priceColor = Colors.amber; | ||||||
|   | |||||||
| @@ -73,11 +73,9 @@ class TokenLoginPopup extends Popup { | |||||||
|                     padding: EdgeInsets.all(20), |                     padding: EdgeInsets.all(20), | ||||||
|                       child: TextFormField( |                       child: TextFormField( | ||||||
|                       onSaved: (newValue) { |                       onSaved: (newValue) { | ||||||
|                         final storage = new FlutterSecureStorage(); |                         Hive.box(AppSettings.DEFAULT_HIVE_BOX).put(AppSettings.AUTH_TOKEN_KEY, newValue.trim()); | ||||||
|                         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', | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| name: hass_client | name: hass_client | ||||||
| description: Home Assistant Android Client | description: Home Assistant Android Client | ||||||
|  |  | ||||||
| version: 0.0.0+1151 | version: 1.1.2+1161 | ||||||
|  |  | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
| @@ -19,12 +19,13 @@ dependencies: | |||||||
|   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 |   in_app_purchase: ^0.3.4 | ||||||
|   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 | ||||||
|   flutter_secure_storage: ^3.3.3 |   hive: ^1.4.1+1 | ||||||
|   device_info: ^0.4.1+4 |   hive_flutter: ^0.3.0+2 | ||||||
|  |   device_info: ^0.4.2+4 | ||||||
|   geolocator: ^5.3.1 |   geolocator: ^5.3.1 | ||||||
|   workmanager: ^0.2.2 |   workmanager: ^0.2.2 | ||||||
|   battery: ^1.0.0 |   battery: ^1.0.0 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user