Compare commits
114 Commits
1.0.1
...
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 | |||
e295a36465 | |||
9a09a83dc6 | |||
95ca80949f | |||
80b5763530 | |||
9a5e35b024 | |||
4493975676 | |||
141a68faf7 | |||
a8efe7dbb6 | |||
9608983994 | |||
8eb15ab9a4 | |||
aac0cfbb56 | |||
343494ece0 | |||
b1e5e73278 | |||
9b5a0068fd | |||
aa26212ddd | |||
1c45f96706 | |||
c2d5192c51 | |||
88ae80507c | |||
55868d1dfe | |||
92a1230267 | |||
d3f99fb262 | |||
3fdf016c39 | |||
8ce0e8aafa | |||
54f6fb28ef | |||
d53825f140 | |||
ea7e0f04ce | |||
cf75989447 | |||
f27d55869b | |||
a287f597ad | |||
0698950f3d | |||
aa58559ba6 | |||
94acc67383 | |||
701e6a46df | |||
1ed56ce8f1 | |||
a6d1baca77 | |||
dbeda6ea68 | |||
3dca28a7da | |||
da4264a409 | |||
302451e118 | |||
d19dbd389b | |||
05ae954b30 | |||
840e266381 | |||
01525a2929 | |||
2eb2596f37 | |||
699cab3498 | |||
353c80b6bc | |||
e4d1a4f823 | |||
78d6b38b92 | |||
1499a91ef7 | |||
9b7f7aa380 | |||
5683ab5158 | |||
a20dfaf05e | |||
24d42c9597 | |||
9078ad81e8 | |||
7cba6c8a10 | |||
c1f9c8c16d | |||
1d1d132b33 | |||
e258b3bc2c | |||
13508ee92f | |||
4fbf58e707 | |||
a3442f84ca | |||
6a6ab3b2cb | |||
d9fa553e2f | |||
cde5d9b912 | |||
3468446b5b | |||
326434273a | |||
470d3be946 | |||
d1032be6a6 | |||
cffac8e1f8 | |||
870bc25dd9 | |||
de713024f6 | |||
4d4add4581 | |||
1670c8e505 | |||
55eb1b5125 | |||
dbeaaaf91e | |||
8166d8ce6d | |||
35bcf0c1fa |
14
README.md
14
README.md
@ -1,18 +1,18 @@
|
||||
# HA Client
|
||||
## Native Android client for Home Assistant
|
||||
### With notifications and Lovelace UI support
|
||||
### With actionable notifications, location tracking and Lovelace UI support
|
||||
|
||||
Visit [ha-client.app](http://ha-client.app/) for more info.
|
||||
|
||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
|
||||
Download the app from [Google Play](https://play.google.com/store/apps/details?id=com.keyboardcrumbs.haclient)
|
||||
|
||||
Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
|
||||
|
||||
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
||||
|
||||
#### Stable CI build
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5eaef46c513b9f9b25eb7f1a/latest_build)
|
||||
#### Beta CI build
|
||||
#### Last release build status
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
|
||||
#### Pre-release CI build
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
|
||||
|
||||
#### Projects used
|
||||
- [HANotify](https://github.com/Crewski/HANotify) by [Crewski](https://github.com/Crewski)
|
||||
- [hassalarm](https://github.com/Johboh/hassalarm) by [Johboh](https://github.com/Johboh) distributed under [MIT License](https://github.com/Johboh/hassalarm/blob/master/LICENSE)
|
||||
|
@ -79,6 +79,8 @@ flutter {
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.firebase:firebase-analytics:17.2.2'
|
||||
implementation 'com.google.firebase:firebase-messaging:20.2.0'
|
||||
implementation 'androidx.work:work-runtime:2.3.4'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
|
@ -1,5 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.keyboardcrumbs.hassclient">
|
||||
package="com.keyboardcrumbs.hassclient"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
@ -16,7 +17,6 @@
|
||||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:label="HA Client"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
@ -43,16 +43,31 @@
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<intent-filter>
|
||||
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".MessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<receiver android:name=".NotificationActionReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".NextAlarmBroadcastReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<service
|
||||
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
@ -64,7 +79,7 @@
|
||||
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
|
||||
android:enabled="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
|
@ -5,11 +5,67 @@ import io.flutter.embedding.android.FlutterActivity;
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.os.Bundle;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
import com.google.android.gms.tasks.OnCompleteListener;
|
||||
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.InstanceIdResult;
|
||||
import com.google.firebase.messaging.FirebaseMessaging;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
|
||||
private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native";
|
||||
|
||||
@Override
|
||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(
|
||||
new MethodChannel.MethodCallHandler() {
|
||||
@Override
|
||||
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
|
||||
Context context = getActivity();
|
||||
if (call.method.equals("getFCMToken")) {
|
||||
if (checkPlayServices()) {
|
||||
FirebaseInstanceId.getInstance().getInstanceId()
|
||||
.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
|
||||
@Override
|
||||
public void onComplete(@NonNull Task<InstanceIdResult> task) {
|
||||
if (task.isSuccessful()) {
|
||||
String token = task.getResult().getToken();
|
||||
UpdateTokenTask updateTokenTask = new UpdateTokenTask(context);
|
||||
updateTokenTask.execute(token);
|
||||
result.success(token);
|
||||
} else {
|
||||
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
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,152 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import java.util.Map;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.io.IOException;
|
||||
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.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.webkit.URLUtil;
|
||||
|
||||
|
||||
public class MessagingService extends FirebaseMessagingService {
|
||||
|
||||
private static final String TAG = "MessagingService";
|
||||
|
||||
@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(String token) {
|
||||
UpdateTokenTask updateTokenTask = new UpdateTokenTask(this);
|
||||
updateTokenTask.execute(token);
|
||||
}
|
||||
|
||||
private void sendNotification(Map<String, String> data) {
|
||||
String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription;
|
||||
boolean autoCancel;
|
||||
if (!data.containsKey("channelId")) {
|
||||
channelId = "ha_notify";
|
||||
channelDescription = "Default notification channel";
|
||||
} else {
|
||||
channelId = data.get("channelId");
|
||||
channelDescription = channelId;
|
||||
}
|
||||
if (!data.containsKey("body")) {
|
||||
messageBody = "";
|
||||
} else {
|
||||
messageBody = data.get("body");
|
||||
}
|
||||
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);
|
||||
if (URLUtil.isValidUrl(imageUrl)) {
|
||||
Bitmap image = getBitmapFromURL(imageUrl);
|
||||
if (image != null) {
|
||||
notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon)));
|
||||
notificationBuilder.setLargeIcon(image);
|
||||
}
|
||||
}
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
if (data.containsKey("action" + i)) {
|
||||
Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class);
|
||||
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.mini_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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Intent;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class NotificationActionReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = "NotificationAction";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null) {
|
||||
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);
|
||||
}
|
||||
SharedPreferences prefs = context.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 actionData = new JSONObject(rawActionData);
|
||||
if (URLUtil.isValidUrl(requestUrl)) {
|
||||
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);
|
||||
String stringRequest = dataToSend.toString();
|
||||
SendTask sendTask = new SendTask();
|
||||
sendTask.execute(requestUrl, stringRequest);
|
||||
} else {
|
||||
Log.w(TAG, "Invalid HA url");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error handling notification action", e);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Webhook id not found");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.util.Log;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class SendTask extends AsyncTask<String, String, String> {
|
||||
|
||||
private static final String TAG = "SendTask";
|
||||
|
||||
public SendTask(){
|
||||
//set context variables if required
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
String urlString = params[0];
|
||||
String data = params[1];
|
||||
|
||||
try {
|
||||
URL url = new URL(urlString);
|
||||
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
|
||||
urlConnection.setRequestMethod("POST");
|
||||
urlConnection.setRequestProperty("Content-Type", "application/json");
|
||||
urlConnection.setDoOutput(true);
|
||||
byte[] outputBytes = data.getBytes("UTF-8");
|
||||
OutputStream os = urlConnection.getOutputStream();
|
||||
os.write(outputBytes);
|
||||
|
||||
int responseCode = urlConnection.getResponseCode();
|
||||
|
||||
urlConnection.disconnect();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error sending data", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.util.Log;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.Context;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
|
||||
public class UpdateTokenTask extends AsyncTask<String, String, String> {
|
||||
|
||||
private static final String TAG = "UpdateTokenTask";
|
||||
|
||||
private WeakReference<Context> contextRef;
|
||||
|
||||
public UpdateTokenTask(Context context){
|
||||
contextRef = new WeakReference<>(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
Log.d(TAG, "Updating push token");
|
||||
Context context = contextRef.get();
|
||||
if (context != null) {
|
||||
String token = params[0];
|
||||
SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putString("flutter.npush-token", token);
|
||||
editor.commit();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
BIN
android/app/src/main/res/drawable/blank_icon.png
Normal file
BIN
android/app/src/main/res/drawable/blank_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 461 B |
@ -1,6 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx2g
|
||||
org.gradle.daemon=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx512m
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.enableR8=true
|
||||
|
@ -11,9 +11,10 @@ window.externalApp.getExternalAuth = function(options) {
|
||||
setTimeout(function(){
|
||||
console.log("Calling a callback");
|
||||
window[options.callback](true, responseData);
|
||||
}, 500);
|
||||
}, 900);
|
||||
}
|
||||
};
|
||||
/*
|
||||
window.externalApp.externalBus = function(message) {
|
||||
console.log("External bus message: " + message);
|
||||
var messageObj = JSON.parse(message);
|
||||
@ -32,4 +33,5 @@ window.externalApp.externalBus = function(message) {
|
||||
} else if (messageObj.type == "config_screen/show") {
|
||||
HAClient.postMessage('show-settings');
|
||||
}
|
||||
};
|
||||
};
|
||||
*/
|
196
lib/cards/badges.dart
Normal file
196
lib/cards/badges.dart
Normal file
@ -0,0 +1,196 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class Badges extends StatelessWidget {
|
||||
final BadgesData badges;
|
||||
|
||||
const Badges({Key key, this.badges}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<EntityWrapper> entitiesToShow = badges.getEntitiesToShow();
|
||||
|
||||
if (entitiesToShow.isNotEmpty) {
|
||||
if (AppSettings().scrollBadges) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.tightFor(height: 112),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: entitiesToShow.map((entity) =>
|
||||
EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||
child: BadgeWidget(),
|
||||
),
|
||||
handleTap: true,
|
||||
)).toList()
|
||||
),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10.0,
|
||||
runSpacing: 5,
|
||||
children: entitiesToShow.map((entity) =>
|
||||
EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: BadgeWidget(),
|
||||
handleTap: true,
|
||||
)).toList(),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
}
|
||||
|
||||
class BadgeWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
Widget badgeIcon;
|
||||
String onBadgeTextValue;
|
||||
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
|
||||
switch (entityModel.entityWrapper.entity.domain) {
|
||||
case "sun":
|
||||
{
|
||||
IconData iconData;
|
||||
if (entityModel.entityWrapper.entity.state == "below_horizon") {
|
||||
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf0dc);
|
||||
} else {
|
||||
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf5a8);
|
||||
}
|
||||
badgeIcon = Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Icon(
|
||||
iconData,
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "camera":
|
||||
case "media_player":
|
||||
case "binary_sensor":
|
||||
{
|
||||
badgeIcon = EntityIcon(
|
||||
imagePadding: EdgeInsets.all(0.0),
|
||||
iconPadding: EdgeInsets.all(10),
|
||||
color: Theme.of(context).textTheme.body2.color
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
{
|
||||
badgeIcon = EntityIcon(
|
||||
imagePadding: EdgeInsets.all(0.0),
|
||||
iconPadding: EdgeInsets.all(10),
|
||||
color: Theme.of(context).textTheme.body2.color
|
||||
);
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
||||
badgeIcon = Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.displayState}",
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.body1
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Widget onBadgeText;
|
||||
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
|
||||
onBadgeText = Container(width: 0.0, height: 0.0);
|
||||
} else {
|
||||
onBadgeText = Container(
|
||||
constraints: BoxConstraints(maxWidth: 50),
|
||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||
child: Text("$onBadgeTextValue",
|
||||
style: Theme.of(context).textTheme.overline.copyWith(
|
||||
color: HAClientTheme().getOnBadgeTextColor()
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis
|
||||
),
|
||||
decoration: new BoxDecoration(
|
||||
color: iconColor,
|
||||
borderRadius: BorderRadius.circular(9.0),
|
||||
)
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Stack(
|
||||
overflow: Overflow.visible,
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: new BoxDecoration(
|
||||
// Circle shape
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).cardColor,
|
||||
// The border you want
|
||||
border: Border.all(
|
||||
width: 2.0,
|
||||
color: iconColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 41,
|
||||
height: 41,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
alignment: Alignment.center,
|
||||
child: badgeIcon,
|
||||
)
|
||||
),
|
||||
Positioned(
|
||||
bottom: -6,
|
||||
child: onBadgeText
|
||||
)
|
||||
],
|
||||
),
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 45),
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.displayName}",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.caption.copyWith(
|
||||
fontSize: 10
|
||||
),
|
||||
softWrap: true,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
onTap: () => entityModel.entityWrapper.handleTap(),
|
||||
onDoubleTap: () => entityModel.entityWrapper.handleDoubleTap(),
|
||||
onLongPress: () => entityModel.entityWrapper.handleHold(),
|
||||
);
|
||||
}
|
||||
}
|
@ -13,17 +13,43 @@ class CardData {
|
||||
|
||||
factory CardData.parse(rawData) {
|
||||
try {
|
||||
if (rawData['type'] == null) {
|
||||
rawData['type'] = CardType.ENTITIES;
|
||||
} else if (!(rawData['type'] is String)) {
|
||||
return CardData(null);
|
||||
}
|
||||
switch (rawData['type']) {
|
||||
case CardType.ENTITIES:
|
||||
case CardType.HISTORY_GRAPH:
|
||||
case CardType.MAP:
|
||||
case CardType.PICTURE_GLANCE:
|
||||
case CardType.SENSOR:
|
||||
case CardType.ENTITY:
|
||||
case CardType.WEATHER_FORECAST:
|
||||
case CardType.PLANT_STATUS:
|
||||
if (rawData['entity'] != null) {
|
||||
rawData['entities'] = [rawData['entity']];
|
||||
}
|
||||
return EntitiesCardData(rawData);
|
||||
break;
|
||||
case CardType.ALARM_PANEL:
|
||||
return AlarmPanelCardData(rawData);
|
||||
break;
|
||||
case CardType.BUTTON:
|
||||
return ButtonCardData(rawData);
|
||||
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.ENTITY_BUTTON:
|
||||
case CardType.BUTTON:
|
||||
case CardType.PICTURE_ENTITY:
|
||||
return ButtonCardData(rawData);
|
||||
break;
|
||||
case CardType.CONDITIONAL:
|
||||
@ -42,6 +68,10 @@ class CardData {
|
||||
return GaugeCardData(rawData);
|
||||
break;
|
||||
case CardType.GLANCE:
|
||||
case CardType.THERMOSTAT:
|
||||
if (rawData['entity'] != null) {
|
||||
rawData['entities'] = [rawData['entity']];
|
||||
}
|
||||
return GlanceCardData(rawData);
|
||||
break;
|
||||
case CardType.HORIZONTAL_STACK:
|
||||
@ -56,14 +86,17 @@ class CardData {
|
||||
case CardType.MEDIA_CONTROL:
|
||||
return MediaControlCardData(rawData);
|
||||
break;
|
||||
case CardType.BADGES:
|
||||
return BadgesData(rawData);
|
||||
break;
|
||||
default:
|
||||
if (rawData.containsKey('entities')) {
|
||||
return EntitiesCardData(rawData);
|
||||
} else if (rawData.containsKey('entity')) {
|
||||
rawData['entities'] = [rawData['entity']];
|
||||
return EntitiesCardData(rawData);
|
||||
if (rawData.containsKey('entity')) {
|
||||
rawData['entities'] = [rawData['entity']];
|
||||
}
|
||||
return CardData(rawData);
|
||||
if (rawData.containsKey('entities') && rawData['entities'] is List) {
|
||||
return EntitiesCardData(rawData);
|
||||
}
|
||||
return CardData(null);
|
||||
}
|
||||
} catch (error, stacktrace) {
|
||||
Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace);
|
||||
@ -73,10 +106,14 @@ class CardData {
|
||||
|
||||
CardData(rawData) {
|
||||
if (rawData != null && rawData is Map) {
|
||||
type = rawData['type'] ?? CardType.ENTITIES;
|
||||
type = rawData['type'];
|
||||
conditions = rawData['conditions'] ?? [];
|
||||
showEmpty = rawData['show_empty'] ?? true;
|
||||
stateFilter = rawData['state_filter'] ?? [];
|
||||
if (rawData.containsKey('state_filter') && rawData['state_filter'] is List) {
|
||||
stateFilter = rawData['state_filter'];
|
||||
} else {
|
||||
stateFilter = [];
|
||||
}
|
||||
} else {
|
||||
type = CardType.UNKNOWN;
|
||||
conditions = [];
|
||||
@ -159,6 +196,64 @@ class CardData {
|
||||
|
||||
}
|
||||
|
||||
class BadgesData extends CardData {
|
||||
|
||||
String title;
|
||||
String icon;
|
||||
bool showHeaderToggle;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return Badges(badges: this);
|
||||
}
|
||||
|
||||
BadgesData(rawData) : super(rawData) {
|
||||
if (rawData['badges'] is List) {
|
||||
rawData['badges'].forEach((dynamic rawBadge) {
|
||||
if (rawBadge is String && HomeAssistant().entities.isExist(rawBadge)) {
|
||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawBadge)));
|
||||
} else if (rawBadge is Map && rawBadge.containsKey('entity') && HomeAssistant().entities.isExist(rawBadge['entity'])) {
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(rawBadge['entity']),
|
||||
overrideName: rawBadge["name"]?.toString(),
|
||||
overrideIcon: rawBadge["icon"],
|
||||
)
|
||||
);
|
||||
} else if (rawBadge is Map && rawBadge.containsKey('entities')) {
|
||||
_parseEntities(rawBadge);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _parseEntities(rawData) {
|
||||
var rawEntities = rawData['entities'] ?? [];
|
||||
rawEntities.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(rawEntity),
|
||||
stateFilter: rawData['state_filter'] ?? [],
|
||||
));
|
||||
}
|
||||
} else if (HomeAssistant().entities.isExist('${rawEntity['entity']}')) {
|
||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
overrideName: rawEntity["name"]?.toString(),
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []),
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EntitiesCardData extends CardData {
|
||||
|
||||
String title;
|
||||
@ -172,12 +267,12 @@ class EntitiesCardData extends CardData {
|
||||
|
||||
EntitiesCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData["title"];
|
||||
icon = rawData['icon'];
|
||||
title = rawData['title']?.toString();
|
||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||
stateColor = rawData['state_color'] ?? false;
|
||||
showHeaderToggle = rawData['show_header_toggle'] ?? false;
|
||||
//Parsing entities
|
||||
var rawEntities = rawData["entities"] ?? [];
|
||||
var rawEntities = rawData['entities'] ?? [];
|
||||
rawEntities.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||
@ -203,7 +298,7 @@ class EntitiesCardData extends CardData {
|
||||
EntityWrapper(
|
||||
entity: Entity.callService(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"],
|
||||
name: rawEntity["name"]?.toString(),
|
||||
service: rawEntity["service"],
|
||||
actionName: rawEntity["action_name"]
|
||||
),
|
||||
@ -222,7 +317,7 @@ class EntitiesCardData extends CardData {
|
||||
entities.add(EntityWrapper(
|
||||
entity: Entity.weblink(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"],
|
||||
name: rawEntity["name"]?.toString(),
|
||||
url: rawEntity["url"]
|
||||
),
|
||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||
@ -235,7 +330,7 @@ class EntitiesCardData extends CardData {
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||
overrideName: rawEntity["name"],
|
||||
overrideName: rawEntity["name"]?.toString(),
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? [],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
@ -262,7 +357,7 @@ class AlarmPanelCardData extends CardData {
|
||||
|
||||
AlarmPanelCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name'];
|
||||
name = rawData['name']?.toString();
|
||||
states = rawData['states'];
|
||||
//Parsing entity
|
||||
var entitiId = rawData["entity"];
|
||||
@ -282,6 +377,45 @@ class AlarmPanelCardData extends CardData {
|
||||
|
||||
}
|
||||
|
||||
class LightCardData extends CardData {
|
||||
|
||||
String name;
|
||||
String icon;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
if (this.entity != null && this.entity.entity is LightEntity) {
|
||||
return LightCard(card: this);
|
||||
}
|
||||
return ErrorCard(
|
||||
errorText: 'Specify an entity from within the light domain.',
|
||||
showReportButton: false,
|
||||
);
|
||||
}
|
||||
|
||||
LightCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name']?.toString();
|
||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||
//Parsing entity
|
||||
var entitiId = rawData["entity"];
|
||||
if (entitiId != null && entitiId is String) {
|
||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(entitiId),
|
||||
overrideName: name,
|
||||
overrideIcon: icon,
|
||||
uiAction: EntityUIAction()..tapAction = EntityUIAction.toggle
|
||||
));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
}
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed('$entitiId')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonCardData extends CardData {
|
||||
|
||||
String name;
|
||||
@ -298,8 +432,8 @@ class ButtonCardData extends CardData {
|
||||
|
||||
ButtonCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name'];
|
||||
icon = rawData['icon'];
|
||||
name = rawData['name']?.toString();
|
||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||
showName = rawData['show_name'] ?? true;
|
||||
showIcon = rawData['show_icon'] ?? true;
|
||||
stateColor = rawData['state_color'] ?? true;
|
||||
@ -361,7 +495,7 @@ class GaugeCardData extends CardData {
|
||||
|
||||
GaugeCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name'];
|
||||
name = rawData['name']?.toString();
|
||||
unit = rawData['unit'];
|
||||
if (rawData['min'] is int) {
|
||||
min = rawData['min'].toDouble();
|
||||
@ -379,7 +513,7 @@ class GaugeCardData extends CardData {
|
||||
}
|
||||
severity = rawData['severity'];
|
||||
//Parsing entity
|
||||
var entitiId = rawData["entity"];
|
||||
var entitiId = rawData["entity"] is List ? rawData["entity"][0] : rawData["entity"];
|
||||
if (entitiId != null && entitiId is String) {
|
||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||
entities.add(EntityWrapper(
|
||||
@ -390,7 +524,7 @@ class GaugeCardData extends CardData {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
}
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
entities.add(EntityWrapper(entity: Entity.missed('$entitiId')));
|
||||
}
|
||||
|
||||
}
|
||||
@ -413,7 +547,7 @@ class GlanceCardData extends CardData {
|
||||
|
||||
GlanceCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData["title"];
|
||||
title = rawData["title"]?.toString();
|
||||
showName = rawData['show_name'] ?? true;
|
||||
showIcon = rawData['show_icon'] ?? true;
|
||||
showState = rawData['show_state'] ?? true;
|
||||
@ -435,7 +569,7 @@ class GlanceCardData extends CardData {
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
stateColor: stateColor,
|
||||
overrideName: rawEntity["name"],
|
||||
overrideName: rawEntity["name"]?.toString(),
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? [],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
@ -460,7 +594,7 @@ class HorizontalStackCardData extends CardData {
|
||||
}
|
||||
|
||||
HorizontalStackCardData(rawData) : super(rawData) {
|
||||
if (rawData.containsKey('cards')) {
|
||||
if (rawData.containsKey('cards') && rawData['cards'] is List) {
|
||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||
return CardData.parse(childCard);
|
||||
}).toList();
|
||||
@ -481,7 +615,7 @@ class VerticalStackCardData extends CardData {
|
||||
}
|
||||
|
||||
VerticalStackCardData(rawData) : super(rawData) {
|
||||
if (rawData.containsKey('cards')) {
|
||||
if (rawData.containsKey('cards') && rawData['cards'] is List) {
|
||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||
return CardData.parse(childCard);
|
||||
}).toList();
|
||||
|
@ -62,13 +62,16 @@ class EntityButtonCard extends StatelessWidget {
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
buttonIcon,
|
||||
_buildName(context)
|
||||
],
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
buttonIcon,
|
||||
_buildName(context)
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
|
@ -2,12 +2,21 @@ part of '../main.dart';
|
||||
|
||||
class ErrorCard extends StatelessWidget {
|
||||
final ErrorCardData card;
|
||||
final String errorText;
|
||||
final bool showReportButton;
|
||||
|
||||
const ErrorCard({Key key, this.card}) : super(key: key);
|
||||
const ErrorCard({Key key, this.card, this.errorText, this.showReportButton: true}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String error;
|
||||
if (errorText == null) {
|
||||
error = 'There was an error showing ${card?.type}';
|
||||
} else {
|
||||
error = errorText;
|
||||
}
|
||||
return CardWrapper(
|
||||
color: Theme.of(context).errorColor,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||
child: Column(
|
||||
@ -15,21 +24,25 @@ class ErrorCard extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'There was an error rendering card: ${card.type}. Please copy card config to clipboard and report this issue. Thanks!',
|
||||
error,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
card != null ?
|
||||
RaisedButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(new ClipboardData(text: card.cardConfig));
|
||||
},
|
||||
child: Text('Copy card config'),
|
||||
),
|
||||
) :
|
||||
Container(width: 0, height: 0),
|
||||
showReportButton ?
|
||||
RaisedButton(
|
||||
onPressed: () {
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
|
||||
},
|
||||
child: Text('Report issue'),
|
||||
)
|
||||
) :
|
||||
Container(width: 0, height: 0)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
@ -30,10 +30,13 @@ class GlanceCard extends StatelessWidget {
|
||||
start, end
|
||||
).map(
|
||||
(EntityWrapper entity){
|
||||
return EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: _buildEntityContainer(context, entity),
|
||||
handleTap: true
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: _buildEntityContainer(context, entity),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
).toList()
|
||||
@ -50,17 +53,22 @@ class GlanceCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
return CardWrapper(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
CardHeader(name: card.title),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
|
||||
child: Table(
|
||||
children: rows
|
||||
)
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
CardHeader(
|
||||
name: card.title,
|
||||
emptyPadding: Sizes.rowPadding,
|
||||
),
|
||||
Table(
|
||||
children: rows
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -85,16 +93,14 @@ class GlanceCard extends StatelessWidget {
|
||||
result.add(_buildState());
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: InkResponse(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: result,
|
||||
),
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
return InkResponse(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: result,
|
||||
),
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
);
|
||||
}
|
||||
|
||||
|
161
lib/cards/light_card.dart
Normal file
161
lib/cards/light_card.dart
Normal file
@ -0,0 +1,161 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class LightCard extends StatefulWidget {
|
||||
|
||||
final LightCardData card;
|
||||
|
||||
LightCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _LightCardState();
|
||||
}
|
||||
}
|
||||
|
||||
class _LightCardState extends State<LightCard> {
|
||||
|
||||
double _actualBrightness;
|
||||
double _newBrightness;
|
||||
bool _changedHere = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _setBrightness(double value, LightEntity entity) {
|
||||
setState((){
|
||||
_newBrightness = value;
|
||||
_changedHere = true;
|
||||
});
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "turn_on",
|
||||
entityId: entity.entityId,
|
||||
data: {"brightness": value.round()}
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = widget.card.entity;
|
||||
LightEntity entity = entityWrapper.entity;
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||
return EntityModel(
|
||||
entityWrapper: widget.card.entity,
|
||||
child: MissedEntityWidget(),
|
||||
handleTap: false,
|
||||
);
|
||||
}
|
||||
entityWrapper.overrideName = widget.card.name ??
|
||||
entityWrapper.displayName;
|
||||
entityWrapper.overrideIcon = widget.card.icon ??
|
||||
entityWrapper.icon;
|
||||
|
||||
if (!_changedHere) {
|
||||
_actualBrightness = (entity.brightness ?? 0).toDouble();
|
||||
_newBrightness = _actualBrightness;
|
||||
} else {
|
||||
_changedHere = false;
|
||||
}
|
||||
Color lightColor = entity.color?.toColor();
|
||||
Color color;
|
||||
if (lightColor != null && lightColor != Colors.white) {
|
||||
color = lightColor;
|
||||
} else {
|
||||
color = Theme.of(context).accentColor;
|
||||
}
|
||||
|
||||
return CardWrapper(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: EntityModel(
|
||||
entityWrapper: entityWrapper,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints.loose(Size(200, 200)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
SfRadialGauge(
|
||||
axes: <RadialAxis>[
|
||||
RadialAxis(
|
||||
onAxisTapped: (val) {
|
||||
_setBrightness(val, entity);
|
||||
},
|
||||
maximum: 255,
|
||||
minimum: 0,
|
||||
showLabels: false,
|
||||
showTicks: false,
|
||||
axisLineStyle: AxisLineStyle(
|
||||
thickness: 0.05,
|
||||
thicknessUnit: GaugeSizeUnit.factor,
|
||||
color: HAClientTheme().getDisabledStateColor(context)
|
||||
),
|
||||
pointers: <GaugePointer>[
|
||||
RangePointer(
|
||||
value: _actualBrightness,
|
||||
sizeUnit: GaugeSizeUnit.factor,
|
||||
width: 0.05,
|
||||
color: color,
|
||||
enableAnimation: true,
|
||||
animationType: AnimationType.bounceOut,
|
||||
),
|
||||
MarkerPointer(
|
||||
value: _newBrightness,
|
||||
markerType: MarkerType.circle,
|
||||
markerHeight: 20,
|
||||
markerWidth: 20,
|
||||
enableDragging: true,
|
||||
onValueChangeEnd: (val) {
|
||||
_setBrightness(val, entity);
|
||||
},
|
||||
color: HAClientTheme().getColorByEntityState(entity.state, context)
|
||||
//enableAnimation: true,
|
||||
//animationType: AnimationType.bounceOut,
|
||||
)
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
FractionallySizedBox(
|
||||
heightFactor: 0.4,
|
||||
widthFactor: 0.4,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: InkResponse(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: EntityIcon(
|
||||
showBadge: false,
|
||||
padding: EdgeInsets.all(0)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
EntityName(
|
||||
padding: EdgeInsets.all(0),
|
||||
wordsWrap: true,
|
||||
maxLines: 3,
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,6 @@ class UnsupportedCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CardWrapper(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||
child: Text("'${card.type}' card is not supported yet"),
|
||||
)
|
||||
);
|
||||
return Container(height: 20);
|
||||
}
|
||||
}
|
@ -4,12 +4,14 @@ class CardWrapper extends StatelessWidget {
|
||||
|
||||
final Widget child;
|
||||
final EdgeInsets padding;
|
||||
final Color color;
|
||||
|
||||
const CardWrapper({Key key, this.child, this.padding: const EdgeInsets.all(0)}) : super(key: key);
|
||||
const CardWrapper({Key key, this.child, this.color, this.padding: const EdgeInsets.all(0)}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: color,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: child
|
||||
|
@ -36,66 +36,6 @@ class EntityState {
|
||||
|
||||
}
|
||||
|
||||
class EntityUIAction {
|
||||
static const moreInfo = 'more-info';
|
||||
static const toggle = 'toggle';
|
||||
static const callService = 'call-service';
|
||||
static const navigate = 'navigate';
|
||||
static const none = 'none';
|
||||
|
||||
String tapAction = EntityUIAction.moreInfo;
|
||||
String tapNavigationPath;
|
||||
String tapService;
|
||||
Map<String, dynamic> tapServiceData;
|
||||
String holdAction = EntityUIAction.none;
|
||||
String holdNavigationPath;
|
||||
String holdService;
|
||||
Map<String, dynamic> holdServiceData;
|
||||
String doubleTapAction = EntityUIAction.none;
|
||||
String doubleTapNavigationPath;
|
||||
String doubleTapService;
|
||||
Map<String, dynamic> doubleTapServiceData;
|
||||
|
||||
EntityUIAction({rawEntityData}) {
|
||||
if (rawEntityData != null) {
|
||||
if (rawEntityData["tap_action"] != null) {
|
||||
if (rawEntityData["tap_action"] is String) {
|
||||
tapAction = rawEntityData["tap_action"];
|
||||
} else {
|
||||
tapAction =
|
||||
rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo;
|
||||
tapNavigationPath = rawEntityData["tap_action"]["navigation_path"];
|
||||
tapService = rawEntityData["tap_action"]["service"];
|
||||
tapServiceData = rawEntityData["tap_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
if (rawEntityData["hold_action"] != null) {
|
||||
if (rawEntityData["hold_action"] is String) {
|
||||
holdAction = rawEntityData["hold_action"];
|
||||
} else {
|
||||
holdAction =
|
||||
rawEntityData["hold_action"]["action"] ?? EntityUIAction.none;
|
||||
holdNavigationPath = rawEntityData["hold_action"]["navigation_path"];
|
||||
holdService = rawEntityData["hold_action"]["service"];
|
||||
holdServiceData = rawEntityData["hold_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
if (rawEntityData["double_tap_action"] != null) {
|
||||
if (rawEntityData["double_tap_action"] is String) {
|
||||
doubleTapAction = rawEntityData["double_tap_action"];
|
||||
} else {
|
||||
doubleTapAction =
|
||||
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
|
||||
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
|
||||
doubleTapService = rawEntityData["double_tap_action"]["service"];
|
||||
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CardType {
|
||||
static const HORIZONTAL_STACK = "horizontal-stack";
|
||||
static const VERTICAL_STACK = "vertical-stack";
|
||||
@ -113,6 +53,7 @@ class CardType {
|
||||
static const IFRAME = "iframe";
|
||||
static const GAUGE = "gauge";
|
||||
static const ENTITY_BUTTON = "entity-button";
|
||||
static const ENTITY = "entity";
|
||||
static const BUTTON = "button";
|
||||
static const CONDITIONAL = "conditional";
|
||||
static const ALARM_PANEL = "alarm-panel";
|
||||
@ -120,6 +61,9 @@ class CardType {
|
||||
static const LIGHT = "light";
|
||||
static const ENTITY_FILTER = "entity-filter";
|
||||
static const UNKNOWN = "unknown";
|
||||
static const HISTORY_GRAPH = "history-graph";
|
||||
static const PICTURE_GLANCE = "picture-glance";
|
||||
static const BADGES = "badges";
|
||||
}
|
||||
|
||||
class Sizes {
|
||||
|
@ -1,148 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class BadgeWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
double iconSize = 26.0;
|
||||
Widget badgeIcon;
|
||||
String onBadgeTextValue;
|
||||
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
|
||||
switch (entityModel.entityWrapper.entity.domain) {
|
||||
case "sun":
|
||||
{
|
||||
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
|
||||
? Icon(
|
||||
MaterialDesignIcons.getIconDataFromIconCode(0xf0dc),
|
||||
size: iconSize,
|
||||
)
|
||||
: Icon(
|
||||
MaterialDesignIcons.getIconDataFromIconCode(0xf5a8),
|
||||
size: iconSize,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "camera":
|
||||
case "media_player":
|
||||
case "binary_sensor":
|
||||
{
|
||||
badgeIcon = EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: iconSize,
|
||||
color: Theme.of(context).textTheme.body1.color
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
{
|
||||
badgeIcon = EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: iconSize,
|
||||
color: Theme.of(context).textTheme.body1.color
|
||||
);
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
double stateFontSize;
|
||||
if (entityModel.entityWrapper.entity.displayState.length <= 3) {
|
||||
stateFontSize = 18.0;
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 4) {
|
||||
stateFontSize = 15.0;
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 6) {
|
||||
stateFontSize = 10.0;
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
||||
stateFontSize = 8.0;
|
||||
}
|
||||
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
||||
badgeIcon = Center(
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.displayState}",
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.body1.copyWith(
|
||||
fontSize: stateFontSize
|
||||
)
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Widget onBadgeText;
|
||||
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
|
||||
onBadgeText = Container(width: 0.0, height: 0.0);
|
||||
} else {
|
||||
onBadgeText = Container(
|
||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||
child: Text("$onBadgeTextValue",
|
||||
style: Theme.of(context).textTheme.overline.copyWith(
|
||||
color: HAClientTheme().getOnBadgeTextColor()
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade),
|
||||
decoration: new BoxDecoration(
|
||||
// Circle shape
|
||||
//shape: BoxShape.circle,
|
||||
color: iconColor,
|
||||
borderRadius: BorderRadius.circular(9.0),
|
||||
));
|
||||
}
|
||||
return GestureDetector(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
|
||||
width: 50.0,
|
||||
height: 50.0,
|
||||
decoration: new BoxDecoration(
|
||||
// Circle shape
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).cardColor,
|
||||
// The border you want
|
||||
border: new Border.all(
|
||||
width: 2.0,
|
||||
color: iconColor,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
overflow: Overflow.visible,
|
||||
children: <Widget>[
|
||||
Positioned(
|
||||
width: 46.0,
|
||||
height: 46.0,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
child: badgeIcon,
|
||||
),
|
||||
Positioned(
|
||||
//width: 50.0,
|
||||
bottom: -9.0,
|
||||
left: -10.0,
|
||||
right: -10.0,
|
||||
child: Center(
|
||||
child: onBadgeText,
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 60.0,
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.displayName}",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
softWrap: true,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () =>
|
||||
eventBus.fire(new ShowEntityPageEvent(entityId: entityModel.entityWrapper.entity.entityId)));
|
||||
}
|
||||
}
|
@ -42,7 +42,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
|
||||
_webViewHtml = Uri.dataFromString(
|
||||
file.replaceFirst('{{stream_url}}', '${ConnectionManager().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
||||
file.replaceFirst('{{stream_url}}', '${AppSettings().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
||||
mimeType: 'text/html',
|
||||
encoding: Encoding.getByName('utf-8')
|
||||
).toString();
|
||||
@ -69,7 +69,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
}
|
||||
|
||||
Future _loadMJPEG() async {
|
||||
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||
_streamUrl = '${AppSettings().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||
.entityId}?token=${_entity.attributes['access_token']}';
|
||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||
var file = await rootBundle.loadString('assets/html/cameraView.html');
|
||||
|
@ -11,7 +11,7 @@ class ModeSelectorWidget extends StatelessWidget {
|
||||
ModeSelectorWidget({
|
||||
Key key,
|
||||
@required this.caption,
|
||||
@required this.options,
|
||||
this.options: const [],
|
||||
this.value,
|
||||
@required this.onChange,
|
||||
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||
|
@ -40,8 +40,8 @@ class CoverEntity extends Entity {
|
||||
CoverEntity.SUPPORT_SET_TILT_POSITION);
|
||||
|
||||
|
||||
double get currentPosition => _getDoubleAttributeValue('current_position');
|
||||
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position');
|
||||
double get currentPosition => _getDoubleAttributeValue('current_position') ?? 0;
|
||||
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position') ?? 0;
|
||||
bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0);
|
||||
bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed));
|
||||
bool get canTiltBeOpened => currentTiltPosition < 100;
|
||||
|
@ -78,8 +78,21 @@ class Entity {
|
||||
chartType: EntityHistoryWidgetType.simple
|
||||
);
|
||||
|
||||
String get displayName =>
|
||||
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
|
||||
String get displayName {
|
||||
if (attributes.containsKey('friendly_name')) {
|
||||
return attributes['friendly_name'];
|
||||
}
|
||||
if (attributes.containsKey('name')) {
|
||||
return attributes['name'];
|
||||
}
|
||||
if (entityId == null) {
|
||||
return "";
|
||||
}
|
||||
if (entityId.contains(".")) {
|
||||
return entityId.split(".")[1].replaceAll("_", " ");
|
||||
}
|
||||
return entityId;
|
||||
}
|
||||
|
||||
bool get isView =>
|
||||
(domain == "group") &&
|
||||
@ -209,14 +222,6 @@ class Entity {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBadgeWidget(BuildContext context) {
|
||||
return EntityModel(
|
||||
entityWrapper: EntityWrapper(entity: this),
|
||||
child: BadgeWidget(),
|
||||
handleTap: true,
|
||||
);
|
||||
}
|
||||
|
||||
String getAttribute(String attributeName) {
|
||||
if (attributes != null) {
|
||||
return attributes["$attributeName"].toString();
|
||||
|
@ -3,10 +3,13 @@ part of '../main.dart';
|
||||
class EntityIcon extends StatelessWidget {
|
||||
|
||||
final EdgeInsetsGeometry padding;
|
||||
final EdgeInsetsGeometry iconPadding;
|
||||
final EdgeInsetsGeometry imagePadding;
|
||||
final double size;
|
||||
final Color color;
|
||||
final bool showBadge;
|
||||
|
||||
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key);
|
||||
const EntityIcon({Key key, this.color, this.showBadge: true, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0), this.iconPadding, this.imagePadding}) : super(key: key);
|
||||
|
||||
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
||||
if (entityId == null) {
|
||||
@ -26,76 +29,6 @@ class EntityIcon extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildIcon(BuildContext context, EntityWrapper data, Color color) {
|
||||
Widget result;
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
if (data.entityPicture != null) {
|
||||
result = Container(
|
||||
height: size+12,
|
||||
width: size+12,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
fit:BoxFit.cover,
|
||||
image: CachedNetworkImageProvider(
|
||||
"${data.entityPicture}"
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
} else {
|
||||
String iconName = data.icon;
|
||||
int iconCode = 0;
|
||||
if (iconName.length > 0) {
|
||||
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
||||
} else {
|
||||
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||
data.entity.deviceClass, data.entity.state); //
|
||||
}
|
||||
result = Icon(
|
||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||
size: size,
|
||||
color: color,
|
||||
);
|
||||
if (data.entity is LightEntity &&
|
||||
(data.entity as LightEntity).supportColor &&
|
||||
(data.entity as LightEntity).color != null
|
||||
) {
|
||||
Color lightColor = (data.entity as LightEntity).color.toColor();
|
||||
if (lightColor == Colors.white) {
|
||||
return result;
|
||||
}
|
||||
result = Stack(
|
||||
children: <Widget>[
|
||||
result,
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: size / 3,
|
||||
height: size / 3,
|
||||
decoration: BoxDecoration(
|
||||
color: lightColor,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
spreadRadius: 0,
|
||||
blurRadius: 0,
|
||||
offset: Offset(0.3, 0.3)
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
@ -107,13 +40,99 @@ class EntityIcon extends StatelessWidget {
|
||||
} else {
|
||||
iconColor = HAClientTheme().getOffStateColor(context);
|
||||
}
|
||||
Widget iconWidget;
|
||||
bool isPicture = false;
|
||||
if (entityWrapper == null) {
|
||||
iconWidget = Container(
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
} else {
|
||||
if (entityWrapper.entityPicture != null) {
|
||||
iconWidget = ClipOval(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: '${entityWrapper.entityPicture}',
|
||||
width: size+12,
|
||||
fit: BoxFit.cover,
|
||||
height: size+12,
|
||||
errorWidget: (context, str, dyn) {
|
||||
return Padding(
|
||||
padding: iconPadding ?? padding,
|
||||
child: _buildIcon(entityWrapper, iconColor)
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
isPicture = true;
|
||||
} else {
|
||||
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: padding,
|
||||
child: buildIcon(
|
||||
context,
|
||||
entityWrapper,
|
||||
iconColor
|
||||
),
|
||||
padding: computedPadding,
|
||||
child: iconWidget,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIcon(EntityWrapper entityWrapper, Color iconColor) {
|
||||
Widget iconWidget;
|
||||
String iconName = entityWrapper.icon;
|
||||
int iconCode = 0;
|
||||
if (iconName.length > 0) {
|
||||
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
||||
} else {
|
||||
iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId,
|
||||
entityWrapper.entity.deviceClass, entityWrapper.entity.state); //
|
||||
}
|
||||
if (showBadge && entityWrapper.entity is LightEntity &&
|
||||
(entityWrapper.entity as LightEntity).supportColor &&
|
||||
(entityWrapper.entity as LightEntity).color != null &&
|
||||
(entityWrapper.entity as LightEntity).color.toColor() != Colors.white
|
||||
) {
|
||||
Color lightColor = (entityWrapper.entity as LightEntity).color.toColor();
|
||||
iconWidget = Stack(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||
size: size,
|
||||
color: iconColor,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: size / 3,
|
||||
height: size / 3,
|
||||
decoration: BoxDecoration(
|
||||
color: lightColor,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
spreadRadius: 0,
|
||||
blurRadius: 0,
|
||||
offset: Offset(0.3, 0.3)
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
iconWidget = Icon(
|
||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||
size: size,
|
||||
color: iconColor,
|
||||
);
|
||||
}
|
||||
return iconWidget;
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ part of '../main.dart';
|
||||
class EntityWrapper {
|
||||
|
||||
String overrideName;
|
||||
final String overrideIcon;
|
||||
String overrideIcon;
|
||||
final bool stateColor;
|
||||
EntityUIAction uiAction;
|
||||
Entity entity;
|
||||
@ -61,7 +61,7 @@ class EntityWrapper {
|
||||
case EntityUIAction.navigate: {
|
||||
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
|
||||
//TODO handle local urls
|
||||
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.tapService}');
|
||||
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.tapService}');
|
||||
} else {
|
||||
Launcher.launchURLInBrowser(uiAction.tapService);
|
||||
}
|
||||
@ -101,7 +101,7 @@ class EntityWrapper {
|
||||
case EntityUIAction.navigate: {
|
||||
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
|
||||
//TODO handle local urls
|
||||
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.holdService}');
|
||||
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.holdService}');
|
||||
} else {
|
||||
Launcher.launchURLInBrowser(uiAction.holdService);
|
||||
}
|
||||
@ -141,7 +141,7 @@ class EntityWrapper {
|
||||
case EntityUIAction.navigate: {
|
||||
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
|
||||
//TODO handle local urls
|
||||
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.doubleTapService}');
|
||||
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.doubleTapService}');
|
||||
} else {
|
||||
Launcher.launchURLInBrowser(uiAction.doubleTapService);
|
||||
}
|
||||
@ -154,4 +154,64 @@ class EntityWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EntityUIAction {
|
||||
static const moreInfo = 'more-info';
|
||||
static const toggle = 'toggle';
|
||||
static const callService = 'call-service';
|
||||
static const navigate = 'navigate';
|
||||
static const none = 'none';
|
||||
|
||||
String tapAction = EntityUIAction.moreInfo;
|
||||
String tapNavigationPath;
|
||||
String tapService;
|
||||
Map<String, dynamic> tapServiceData;
|
||||
String holdAction = EntityUIAction.moreInfo;
|
||||
String holdNavigationPath;
|
||||
String holdService;
|
||||
Map<String, dynamic> holdServiceData;
|
||||
String doubleTapAction = EntityUIAction.none;
|
||||
String doubleTapNavigationPath;
|
||||
String doubleTapService;
|
||||
Map<String, dynamic> doubleTapServiceData;
|
||||
|
||||
EntityUIAction({rawEntityData}) {
|
||||
if (rawEntityData != null) {
|
||||
if (rawEntityData["tap_action"] != null) {
|
||||
if (rawEntityData["tap_action"] is String) {
|
||||
tapAction = rawEntityData["tap_action"];
|
||||
} else {
|
||||
tapAction =
|
||||
rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo;
|
||||
tapNavigationPath = rawEntityData["tap_action"]["navigation_path"];
|
||||
tapService = rawEntityData["tap_action"]["service"];
|
||||
tapServiceData = rawEntityData["tap_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
if (rawEntityData["hold_action"] != null) {
|
||||
if (rawEntityData["hold_action"] is String) {
|
||||
holdAction = rawEntityData["hold_action"];
|
||||
} else {
|
||||
holdAction =
|
||||
rawEntityData["hold_action"]["action"] ?? EntityUIAction.none;
|
||||
holdNavigationPath = rawEntityData["hold_action"]["navigation_path"];
|
||||
holdService = rawEntityData["hold_action"]["service"];
|
||||
holdServiceData = rawEntityData["hold_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
if (rawEntityData["double_tap_action"] != null) {
|
||||
if (rawEntityData["double_tap_action"] is String) {
|
||||
doubleTapAction = rawEntityData["double_tap_action"];
|
||||
} else {
|
||||
doubleTapAction =
|
||||
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
|
||||
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
|
||||
doubleTapService = rawEntityData["double_tap_action"]["service"];
|
||||
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -134,8 +134,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
_tmpBrightness = value.round();
|
||||
});
|
||||
},
|
||||
min: 1.0,
|
||||
max: 255.0,
|
||||
min: 1,
|
||||
max: 255,
|
||||
divisions: 254,
|
||||
label: '${val?.toInt() ?? ''}',
|
||||
onChangeEnd: (value) => _setBrightness(entity, value),
|
||||
value: val,
|
||||
leading: Icon(Icons.brightness_5),
|
||||
@ -155,10 +157,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
_tmpWhiteValue = value.round();
|
||||
});
|
||||
},
|
||||
min: 0.0,
|
||||
max: 255.0,
|
||||
min: 0,
|
||||
max: 255,
|
||||
divisions: 255,
|
||||
label: '$_tmpWhiteValue',
|
||||
onChangeEnd: (value) => _setWhiteValue(entity, value),
|
||||
value: _tmpWhiteValue == null ? 0.0 : _tmpWhiteValue.toDouble(),
|
||||
value: _tmpWhiteValue?.toDouble() ?? 0.0,
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")),
|
||||
title: "White",
|
||||
);
|
||||
@ -188,6 +192,8 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
onChangeEnd: (value) => _setColorTemp(entity, value),
|
||||
max: entity.maxMireds,
|
||||
min: entity.minMireds,
|
||||
divisions: (entity.maxMireds - entity.minMireds).toInt(),
|
||||
label: '$_tmpColorTemp',
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
|
@ -8,8 +8,8 @@ class TimerEntity extends Entity {
|
||||
@override
|
||||
void update(Map rawData, String webHost) {
|
||||
super.update(rawData, webHost);
|
||||
String durationSource = "${attributes["duration"]}";
|
||||
if (durationSource != null && durationSource.isNotEmpty) {
|
||||
if (attributes.containsKey('duration')) {
|
||||
String durationSource = "${attributes["duration"]}";
|
||||
try {
|
||||
List<String> durationList = durationSource.split(":");
|
||||
if (durationList.length == 1) {
|
||||
@ -26,11 +26,11 @@ class TimerEntity extends Entity {
|
||||
seconds: int.tryParse(durationList[2]) ?? 0
|
||||
);
|
||||
} else {
|
||||
Logger.e("Strange $entityId duration format: $durationSource");
|
||||
Logger.e("Strange timer duration format: $durationSource");
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e("Error parsing duration for $entityId: $e", stacktrace: stacktrace);
|
||||
Logger.e("Error parsing timer duration \'$durationSource\': $e", stacktrace: stacktrace);
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
} else {
|
||||
|
@ -13,9 +13,10 @@ class UniversalSlider extends StatefulWidget {
|
||||
final double max;
|
||||
final double value;
|
||||
final int divisions;
|
||||
final String label;
|
||||
final EdgeInsets padding;
|
||||
|
||||
const UniversalSlider({Key key, this.onChanged, this.onChangeStart, this.activeColor, this.divisions, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
||||
const UniversalSlider({Key key, this.onChanged, this.label, this.onChangeStart, this.activeColor, this.divisions, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
@ -28,10 +29,10 @@ class UniversalSliderState extends State<UniversalSlider> {
|
||||
|
||||
double _value;
|
||||
bool _changeStarted = false;
|
||||
bool _changedHere = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_value = widget.value;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -39,6 +40,11 @@ class UniversalSliderState extends State<UniversalSlider> {
|
||||
Widget build(BuildContext context) {
|
||||
List <Widget> row = [];
|
||||
List <Widget> col = [];
|
||||
if (!_changedHere) {
|
||||
_value = widget.value;
|
||||
} else {
|
||||
_changedHere = false;
|
||||
}
|
||||
if (widget.leading != null) {
|
||||
row.add(widget.leading);
|
||||
}
|
||||
@ -49,6 +55,7 @@ class UniversalSliderState extends State<UniversalSlider> {
|
||||
min: widget.min ?? 0,
|
||||
max: widget.max ?? 100,
|
||||
activeColor: widget.activeColor,
|
||||
label: widget.label,
|
||||
onChangeStart: (value) {
|
||||
_changeStarted = true;
|
||||
widget.onChangeStart?.call(value);
|
||||
@ -57,6 +64,7 @@ class UniversalSliderState extends State<UniversalSlider> {
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_value = value;
|
||||
_changedHere = true;
|
||||
});
|
||||
widget.onChanged?.call(value);
|
||||
},
|
||||
@ -64,6 +72,7 @@ class UniversalSliderState extends State<UniversalSlider> {
|
||||
_changeStarted = false;
|
||||
setState(() {
|
||||
_value = value;
|
||||
_changedHere = true;
|
||||
});
|
||||
Timer(Duration(milliseconds: 500), () {
|
||||
if (!_changeStarted) {
|
||||
|
@ -149,7 +149,7 @@ class EntityCollection {
|
||||
}
|
||||
|
||||
bool isExist(String entityId) {
|
||||
return _allEntities[entityId] != null;
|
||||
return _allEntities.containsKey(entityId);
|
||||
}
|
||||
|
||||
List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) {
|
||||
|
@ -14,7 +14,7 @@ class HomeAssistant {
|
||||
HomeAssistantUI ui;
|
||||
Map _instanceConfig = {};
|
||||
String _userName;
|
||||
String _lovelaceDashbordUrl;
|
||||
String currentDashboardPath;
|
||||
HSVColor savedColor;
|
||||
int savedPlayerPosition;
|
||||
String sendToPlayerId;
|
||||
@ -22,15 +22,11 @@ class HomeAssistant {
|
||||
Map services;
|
||||
bool autoUi = false;
|
||||
|
||||
String fcmToken;
|
||||
|
||||
Map _rawLovelaceData;
|
||||
var _rawStates;
|
||||
var _rawUserInfo;
|
||||
var _rawPanels;
|
||||
|
||||
set lovelaceDashboardUrl(String val) => _lovelaceDashbordUrl = val;
|
||||
|
||||
List<Panel> panels = [];
|
||||
|
||||
Duration fetchTimeout = Duration(seconds: 30);
|
||||
@ -63,7 +59,7 @@ class HomeAssistant {
|
||||
_fetchCompleter = Completer();
|
||||
List<Future> futures = [];
|
||||
if (!uiOnly) {
|
||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||
if (entities == null) entities = EntityCollection(AppSettings().httpWebHost);
|
||||
futures.add(_getStates(null));
|
||||
futures.add(_getConfig(null));
|
||||
futures.add(_getUserInfo(null));
|
||||
@ -77,7 +73,6 @@ class HomeAssistant {
|
||||
if (isComponentEnabled('mobile_app')) {
|
||||
_createUI();
|
||||
_fetchCompleter.complete();
|
||||
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
|
||||
} else {
|
||||
_fetchCompleter.completeError(HACException("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")]));
|
||||
}
|
||||
@ -92,7 +87,7 @@ class HomeAssistant {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
bool cached = prefs.getBool('cached');
|
||||
if (cached != null && cached) {
|
||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||
if (entities == null) entities = EntityCollection(AppSettings().httpWebHost);
|
||||
try {
|
||||
_getStates(prefs);
|
||||
if (!autoUi) {
|
||||
@ -163,7 +158,7 @@ class HomeAssistant {
|
||||
if (sharedPrefs != null && sharedPrefs.containsKey('cached_states')) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_states'));
|
||||
_parseStates(data);
|
||||
_parseStates(data ?? []);
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e('Error getting cached states: $e', stacktrace: stacktrace);
|
||||
}
|
||||
@ -194,9 +189,9 @@ class HomeAssistant {
|
||||
} else {
|
||||
Completer completer = Completer();
|
||||
var additionalData;
|
||||
if (_lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
|
||||
if (AppSettings().haVersion >= 107 && currentDashboardPath != HomeAssistant.DEFAULT_DASHBOARD) {
|
||||
additionalData = {
|
||||
'url_path': _lovelaceDashbordUrl
|
||||
'url_path': currentDashboardPath
|
||||
};
|
||||
}
|
||||
ConnectionManager().sendSocketMessage(
|
||||
@ -224,9 +219,9 @@ class HomeAssistant {
|
||||
if (prefs != null && prefs.containsKey('cached_services')) {
|
||||
try {
|
||||
var data = json.decode(prefs.getString('cached_services'));
|
||||
_parseServices(data);
|
||||
_parseServices(data ?? {});
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e(e, stacktrace: stacktrace);
|
||||
Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true);
|
||||
}
|
||||
}
|
||||
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
|
||||
@ -243,7 +238,7 @@ class HomeAssistant {
|
||||
if (sharedPrefs != null && sharedPrefs.containsKey('cached_user')) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_user'));
|
||||
_parseUserInfo(data);
|
||||
_parseUserInfo(data ?? {});
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e('Error getting cached user info: $e', stacktrace: stacktrace);
|
||||
}
|
||||
@ -264,9 +259,9 @@ class HomeAssistant {
|
||||
if (sharedPrefs != null && sharedPrefs.containsKey('cached_panels')) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_panels'));
|
||||
_parsePanels(data);
|
||||
_parsePanels(data ?? {});
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e(e, stacktrace: stacktrace);
|
||||
Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true);
|
||||
panels.clear();
|
||||
}
|
||||
} else {
|
||||
|
@ -16,10 +16,9 @@ import 'package:http/http.dart' as http;
|
||||
import 'package:charts_flutter/flutter.dart' as charts;
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:device_info/device_info.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'plugins/dynamic_multi_column_layout.dart';
|
||||
import 'plugins/spoiler_card.dart';
|
||||
@ -58,7 +57,6 @@ part 'entities/fan/fan_entity.class.dart';
|
||||
part 'entities/automation/automation_entity.class.dart';
|
||||
part 'entities/camera/camera_entity.class.dart';
|
||||
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
|
||||
part 'entities/badge.widget.dart';
|
||||
part 'entities/entity_model.widget.dart';
|
||||
part 'entities/default_entity_container.widget.dart';
|
||||
part 'entities/missed_entity.widget.dart';
|
||||
@ -143,6 +141,7 @@ part 'cards/horizontal_srack_card.dart';
|
||||
part 'cards/markdown_card.dart';
|
||||
part 'cards/media_control_card.dart';
|
||||
part 'cards/unsupported_card.dart';
|
||||
part 'cards/light_card.dart';
|
||||
part 'cards/error_card.dart';
|
||||
part 'cards/vertical_stack_card.dart';
|
||||
part 'cards/glance_card.dart';
|
||||
@ -153,15 +152,13 @@ part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
|
||||
part 'pages/whats_new.page.dart';
|
||||
part 'pages/fullscreen.page.dart';
|
||||
part 'popups.dart';
|
||||
part 'cards/badges.dart';
|
||||
part 'managers/app_settings.dart';
|
||||
|
||||
EventBus eventBus = new EventBus();
|
||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||
const String appName = 'HA Client';
|
||||
const appVersionNumber = '1.0.1';
|
||||
final String appVersionAdd = secrets['version_type'] ?? '';
|
||||
final String appVersion = '$appVersionNumber${appVersionAdd.isNotEmpty ? '-' : ''}$appVersionAdd';
|
||||
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.0.1.md';
|
||||
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0');
|
||||
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.2.0.md';
|
||||
|
||||
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
||||
// Print the exception to the console.
|
||||
@ -186,12 +183,17 @@ void main() async {
|
||||
};
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
AppTheme theme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
|
||||
await AppSettings().loadStartupSettings();
|
||||
await Hive.initFlutter();
|
||||
if (AppSettings().displayMode == DisplayMode.fullscreen) {
|
||||
SystemChrome.setEnabledSystemUIOverlays([]);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||
}
|
||||
|
||||
runZoned(() {
|
||||
runApp(new HAClientApp(
|
||||
theme: theme,
|
||||
theme: AppSettings().appTheme,
|
||||
));
|
||||
}, onError: (error, stack) {
|
||||
_reportError(error, stack);
|
||||
@ -245,6 +247,7 @@ class _HAClientAppState extends State<HAClientApp> {
|
||||
positiveText: "Ok"
|
||||
)
|
||||
));
|
||||
InAppPurchaseConnection.instance.completePurchase(purchase[0]);
|
||||
} else {
|
||||
Logger.d("Purchase change handler: ${purchase[0].status}");
|
||||
}
|
||||
@ -253,7 +256,6 @@ class _HAClientAppState extends State<HAClientApp> {
|
||||
}
|
||||
}
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new MaterialApp(
|
||||
@ -286,7 +288,7 @@ class _HAClientAppState extends State<HAClientApp> {
|
||||
"/quick-start": (context) => QuickStartPage(),
|
||||
"/haclient_zha": (context) => ZhaPage(),
|
||||
"/auth": (context) => new standaloneWebview.WebviewScaffold(
|
||||
url: "${ConnectionManager().oauthUrl}",
|
||||
url: "${AppSettings().oauthUrl}",
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.help),
|
||||
|
127
lib/managers/app_settings.dart
Normal file
127
lib/managers/app_settings.dart
Normal file
@ -0,0 +1,127 @@
|
||||
part of '../main.dart';
|
||||
|
||||
enum DisplayMode {normal, fullscreen}
|
||||
|
||||
class AppSettings {
|
||||
|
||||
static const DEFAULT_HIVE_BOX = 'defaultSettingsBox';
|
||||
|
||||
static const AUTH_TOKEN_KEY = 'llt';
|
||||
|
||||
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;
|
||||
final int defaultLocationUpdateIntervalMinutes = 20;
|
||||
Duration locationUpdateInterval;
|
||||
bool locationTrackingEnabled = false;
|
||||
|
||||
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();
|
||||
_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";
|
||||
locationUpdateInterval = Duration(minutes: prefs.getInt("location-interval") ??
|
||||
defaultLocationUpdateIntervalMinutes);
|
||||
locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||
nextAlarmSensorCreated = prefs.getBool("next-alarm-sensor-created") ?? false;
|
||||
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<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);
|
||||
}
|
||||
|
||||
}
|
@ -10,24 +10,11 @@ class ConnectionManager {
|
||||
|
||||
ConnectionManager._internal();
|
||||
|
||||
String _domain;
|
||||
String _port;
|
||||
String displayHostname;
|
||||
String _webSocketAPIEndpoint;
|
||||
String httpWebHost;
|
||||
String _token;
|
||||
String _tempToken;
|
||||
String oauthUrl;
|
||||
String webhookId;
|
||||
double haVersion;
|
||||
String mobileAppDeviceName;
|
||||
bool settingsLoaded = false;
|
||||
int appIntegrationVersion;
|
||||
bool get isAuthenticated => _token != null;
|
||||
StreamSubscription _socketSubscription;
|
||||
Duration connectTimeout = Duration(seconds: 15);
|
||||
|
||||
bool isConnected = false;
|
||||
bool settingsLoaded = false;
|
||||
|
||||
var onStateChangeCallback;
|
||||
var onLovelaceUpdatedCallback;
|
||||
@ -37,69 +24,27 @@ class ConnectionManager {
|
||||
int _currentMessageId = 0;
|
||||
Map<String, Completer> _messageResolver = {};
|
||||
|
||||
Future init({bool loadSettings, bool forceReconnect: false}) async {
|
||||
Future init({bool loadSettings, bool forceReconnect: false}) {
|
||||
Completer completer = Completer();
|
||||
bool stopInit = false;
|
||||
if (loadSettings) {
|
||||
Logger.d("Loading settings...");
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
_domain = prefs.getString('hassio-domain');
|
||||
_port = prefs.getString('hassio-port');
|
||||
webhookId = prefs.getString('app-webhook-id');
|
||||
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
|
||||
mobileAppDeviceName = prefs.getString('app-integration-device-name');
|
||||
displayHostname = "$_domain:$_port";
|
||||
_webSocketAPIEndpoint =
|
||||
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
||||
httpWebHost =
|
||||
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
|
||||
Logger.d('$_domain$_port');
|
||||
if (_domain == null && _port == null && webhookId == null && mobileAppDeviceName == null) {
|
||||
AppSettings().load(loadSettings).then((_) {
|
||||
Logger.d('Checking config...');
|
||||
if (AppSettings().isNotConfigured()) {
|
||||
Logger.d('This is first start');
|
||||
completer.completeError(HACNotSetUpException());
|
||||
stopInit = true;
|
||||
} else if ((_domain == null) || (_port == null) ||
|
||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||
} else if (AppSettings().isSomethingMissed()) {
|
||||
completer.completeError(HACException.checkConnectionSettings());
|
||||
stopInit = true;
|
||||
} else {
|
||||
final storage = new FlutterSecureStorage();
|
||||
try {
|
||||
_token = await storage.read(key: "hacl_llt");
|
||||
Logger.d("Long-lived token read successful");
|
||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||
'https://ha-client.app')}&redirect_uri=${Uri
|
||||
.encodeComponent(
|
||||
'https://ha-client.app/service/auth_callback.html')}";
|
||||
settingsLoaded = true;
|
||||
} catch (e, stacktrace) {
|
||||
completer.completeError(HACException("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
|
||||
Logger.e("Error reading secure storage: $e", stacktrace: stacktrace);
|
||||
stopInit = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((_domain == null) || (_port == null) ||
|
||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||
completer.completeError(HACException.checkConnectionSettings());
|
||||
stopInit = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!stopInit) {
|
||||
if (_token == null) {
|
||||
AuthManager().start(
|
||||
oauthUrl: oauthUrl
|
||||
).then((token) {
|
||||
Logger.d("Token from AuthManager recived");
|
||||
_tempToken = token;
|
||||
} else if (!AppSettings().isAuthenticated) {
|
||||
settingsLoaded = true;
|
||||
AppSettings().startAuth().then((_) {
|
||||
_doConnect(completer: completer, forceReconnect: forceReconnect);
|
||||
}).catchError((e) {
|
||||
completer.completeError(e);
|
||||
});
|
||||
} else {
|
||||
settingsLoaded = true;
|
||||
_doConnect(completer: completer, forceReconnect: forceReconnect);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
@ -141,7 +86,7 @@ class ConnectionManager {
|
||||
Logger.d("Socket connecting...");
|
||||
try {
|
||||
_socket = IOWebSocketChannel.connect(
|
||||
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
||||
AppSettings().webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
||||
_socketSubscription = _socket.stream.listen(
|
||||
(message) {
|
||||
isConnected = true;
|
||||
@ -157,9 +102,9 @@ class ConnectionManager {
|
||||
} else if (data["type"] == "auth_ok") {
|
||||
String v = data["ha_version"];
|
||||
if (v != null && v.isNotEmpty) {
|
||||
haVersion = double.tryParse(v.replaceFirst('0.','')) ?? 0;
|
||||
AppSettings().haVersion = double.tryParse(v.replaceFirst('0.','')) ?? 0;
|
||||
}
|
||||
Logger.d("Home assistant version: $v ($haVersion)");
|
||||
Logger.d("Home assistant version: $v (${AppSettings().haVersion})");
|
||||
Crashlytics.instance.setString('ha_version', v);
|
||||
Logger.d("[Connection] Subscribing to events");
|
||||
sendSocketMessage(
|
||||
@ -172,7 +117,7 @@ class ConnectionManager {
|
||||
).whenComplete((){
|
||||
_messageResolver["auth"]?.complete();
|
||||
_messageResolver.remove("auth");
|
||||
if (_token != null) {
|
||||
if (AppSettings().isAuthenticated) {
|
||||
if (!connecting.isCompleted) connecting.complete();
|
||||
}
|
||||
});
|
||||
@ -266,21 +211,21 @@ class ConnectionManager {
|
||||
|
||||
Future _authenticate() {
|
||||
Completer completer = Completer();
|
||||
if (_token != null) {
|
||||
if (AppSettings().isAuthenticated) {
|
||||
Logger.d( "Long-lived token exist");
|
||||
Logger.d( "[Sending] ==> auth request");
|
||||
sendSocketMessage(
|
||||
type: "auth",
|
||||
additionalData: {"access_token": "$_token"},
|
||||
additionalData: {"access_token": "${AppSettings().longLivedToken}"},
|
||||
auth: true
|
||||
).then((_) {
|
||||
completer.complete();
|
||||
}).catchError((e) => completer.completeError(e));
|
||||
} else if (_tempToken != null) {
|
||||
} else if (AppSettings().isTempAuthenticated != null) {
|
||||
Logger.d("We have temp token. Loging in...");
|
||||
sendSocketMessage(
|
||||
type: "auth",
|
||||
additionalData: {"access_token": "$_tempToken"},
|
||||
additionalData: {"access_token": "${AppSettings().tempToken}"},
|
||||
auth: true
|
||||
).then((_) {
|
||||
Logger.d("Requesting long-lived token...");
|
||||
@ -298,35 +243,18 @@ class ConnectionManager {
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future logout() {
|
||||
Future logout() async {
|
||||
Logger.d("Logging out");
|
||||
Completer completer = Completer();
|
||||
_disconnect().whenComplete(() {
|
||||
_token = null;
|
||||
_tempToken = null;
|
||||
final storage = new FlutterSecureStorage();
|
||||
storage.delete(key: "hacl_llt").whenComplete((){
|
||||
completer.complete();
|
||||
});
|
||||
});
|
||||
return completer.future;
|
||||
await _disconnect();
|
||||
await AppSettings().clearTokens();
|
||||
}
|
||||
|
||||
Future _getLongLivedToken() {
|
||||
Completer completer = Completer();
|
||||
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
|
||||
Logger.d("Got long-lived token.");
|
||||
_token = data;
|
||||
_tempToken = null;
|
||||
final storage = new FlutterSecureStorage();
|
||||
storage.write(key: "hacl_llt", value: "$_token").then((_) {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool("oauth-used", true);
|
||||
completer.complete();
|
||||
});
|
||||
}).catchError((e) {
|
||||
throw e;
|
||||
});
|
||||
AppSettings().saveLongLivedToken(data);
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
completer.completeError(HACException("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||
});
|
||||
@ -398,11 +326,11 @@ class ConnectionManager {
|
||||
DateTime now = DateTime.now();
|
||||
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||
String url = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
|
||||
String url = "${AppSettings().httpWebHost}/api/history/period/$startTime?&filter_entity_id=$entityId";
|
||||
Logger.d("[Sending] ==> HTTP /api/history/period/$startTime?&filter_entity_id=$entityId");
|
||||
http.Response historyResponse;
|
||||
historyResponse = await http.get(url, headers: {
|
||||
"authorization": "Bearer $_token",
|
||||
"authorization": "Bearer ${AppSettings().longLivedToken}",
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
var history = json.decode(historyResponse.body);
|
||||
@ -416,14 +344,14 @@ class ConnectionManager {
|
||||
|
||||
Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async {
|
||||
Completer completer = Completer();
|
||||
String url = "$httpWebHost$endPoint";
|
||||
String url = "${AppSettings().httpWebHost}$endPoint";
|
||||
Logger.d("[Sending] ==> HTTP $endPoint");
|
||||
Map<String, String> headers = {};
|
||||
if (contentType != null) {
|
||||
headers["Content-Type"] = contentType;
|
||||
}
|
||||
if (includeAuthHeader) {
|
||||
headers["authorization"] = "Bearer $_token";
|
||||
headers["authorization"] = "Bearer ${AppSettings().longLivedToken}";
|
||||
}
|
||||
http.post(
|
||||
url,
|
||||
|
@ -13,68 +13,53 @@ class LocationManager {
|
||||
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) {
|
||||
if (AppSettings().locationTrackingEnabled) {
|
||||
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 (interval != AppSettings().locationUpdateInterval.inMinutes) {
|
||||
await _stopLocationService();
|
||||
}
|
||||
if (enabled && !_isRunning) {
|
||||
AppSettings().save({
|
||||
'location-interval': interval,
|
||||
'location-enabled': enabled
|
||||
});
|
||||
AppSettings().locationUpdateInterval = Duration(minutes: interval);
|
||||
AppSettings().locationTrackingEnabled = enabled;
|
||||
if (enabled && !AppSettings().locationTrackingEnabled) {
|
||||
Logger.d("Starting location tracking");
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.setBool("location-enabled", enabled);
|
||||
_isRunning = true;
|
||||
await _startLocationService();
|
||||
} else if (!enabled && _isRunning) {
|
||||
} else if (!enabled && AppSettings().locationTrackingEnabled) {
|
||||
Logger.d("Stopping location tracking...");
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.setBool("location-enabled", enabled);
|
||||
_isRunning = false;
|
||||
await _stopLocationService();
|
||||
}
|
||||
}
|
||||
|
||||
_startLocationService() async {
|
||||
String webhookId = ConnectionManager().webhookId;
|
||||
String httpWebHost = ConnectionManager().httpWebHost;
|
||||
String webhookId = AppSettings().webhookId;
|
||||
String httpWebHost = AppSettings().httpWebHost;
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
Duration interval;
|
||||
int delayFactor;
|
||||
int taskCount;
|
||||
Logger.d("Starting location update for every ${_updateInterval
|
||||
Logger.d("Starting location update for every ${AppSettings().locationUpdateInterval
|
||||
.inMinutes} minutes...");
|
||||
if (_updateInterval.inMinutes == 10) {
|
||||
if (AppSettings().locationUpdateInterval.inMinutes == 10) {
|
||||
interval = Duration(minutes: 20);
|
||||
taskCount = 2;
|
||||
delayFactor = 10;
|
||||
} else if (_updateInterval.inMinutes == 5) {
|
||||
} else if (AppSettings().locationUpdateInterval.inMinutes == 5) {
|
||||
interval = Duration(minutes: 15);
|
||||
taskCount = 3;
|
||||
delayFactor = 5;
|
||||
} else {
|
||||
interval = _updateInterval;
|
||||
interval = AppSettings().locationUpdateInterval;
|
||||
taskCount = 1;
|
||||
delayFactor = 0;
|
||||
}
|
||||
@ -112,8 +97,8 @@ class LocationManager {
|
||||
Logger.d("[Foreground location] Started");
|
||||
Geolocator geolocator = Geolocator();
|
||||
var battery = Battery();
|
||||
String webhookId = ConnectionManager().webhookId;
|
||||
String httpWebHost = ConnectionManager().httpWebHost;
|
||||
String webhookId = AppSettings().webhookId;
|
||||
String httpWebHost = AppSettings().httpWebHost;
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
Logger.d("[Foreground location] Getting battery level...");
|
||||
int batteryLevel = await battery.batteryLevel;
|
||||
|
@ -2,17 +2,15 @@ part of '../main.dart';
|
||||
|
||||
class MobileAppIntegrationManager {
|
||||
|
||||
static const INTEGRATION_VERSION = 3;
|
||||
|
||||
static final _appRegistrationData = {
|
||||
"device_name": "",
|
||||
"app_version": "$appVersion",
|
||||
"manufacturer": DeviceInfoManager().manufacturer,
|
||||
"model": DeviceInfoManager().model,
|
||||
"os_version": DeviceInfoManager().osVersion,
|
||||
"manufacturer": DeviceInfoManager().manufacturer ?? "unknown",
|
||||
"model": DeviceInfoManager().model ?? "unknown",
|
||||
"os_version": DeviceInfoManager().osVersion ?? "0",
|
||||
"app_data": {
|
||||
"push_token": "",
|
||||
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
|
||||
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV3"
|
||||
}
|
||||
};
|
||||
|
||||
@ -23,11 +21,29 @@ class MobileAppIntegrationManager {
|
||||
return '${HomeAssistant().userName}\'s ${DeviceInfoManager().model}';
|
||||
}
|
||||
|
||||
static Future checkAppRegistration() {
|
||||
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
|
||||
|
||||
static Future checkAppRegistration() async {
|
||||
String fcmToken = await AppSettings().loadSingle('npush-token');
|
||||
if (fcmToken != null) {
|
||||
Logger.d("[MobileAppIntegrationManager] token exist");
|
||||
await _doCheck(fcmToken);
|
||||
} else {
|
||||
Logger.d("[MobileAppIntegrationManager] no fcm token. Requesting...");
|
||||
try {
|
||||
fcmToken = await platform.invokeMethod('getFCMToken');
|
||||
await _doCheck(fcmToken);
|
||||
} on PlatformException catch (e) {
|
||||
Logger.e('[MobileAppIntegrationManager] Failed to get FCM token from native: ${e.message}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future _doCheck(String fcmToken) {
|
||||
Completer completer = Completer();
|
||||
_appRegistrationData["device_name"] = ConnectionManager().mobileAppDeviceName ?? getDefaultDeviceName();
|
||||
(_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
|
||||
if (ConnectionManager().webhookId == null) {
|
||||
_appRegistrationData["device_name"] = AppSettings().mobileAppDeviceName ?? getDefaultDeviceName();
|
||||
(_appRegistrationData["app_data"] as Map)["push_token"] = "$fcmToken";
|
||||
if (AppSettings().webhookId == null) {
|
||||
Logger.d("Mobile app was not registered yet. Registering...");
|
||||
var registrationData = Map.from(_appRegistrationData);
|
||||
registrationData.addAll({
|
||||
@ -36,7 +52,7 @@ class MobileAppIntegrationManager {
|
||||
"os_name": DeviceInfoManager().osName,
|
||||
"supports_encryption": false,
|
||||
});
|
||||
if (ConnectionManager().haVersion >= 104) {
|
||||
if (AppSettings().haVersion >= 104) {
|
||||
registrationData.addAll({
|
||||
"device_id": "${DeviceInfoManager().unicDeviceId}"
|
||||
});
|
||||
@ -46,14 +62,13 @@ class MobileAppIntegrationManager {
|
||||
includeAuthHeader: true,
|
||||
data: json.encode(registrationData)
|
||||
).then((response) {
|
||||
Logger.d("Processing registration responce...");
|
||||
Logger.d("Processing registration response...");
|
||||
var responseObject = json.decode(response);
|
||||
ConnectionManager().webhookId = responseObject["webhook_id"];
|
||||
ConnectionManager().appIntegrationVersion = INTEGRATION_VERSION;
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
||||
prefs.setInt('app-integration-version', INTEGRATION_VERSION);
|
||||
|
||||
AppSettings().webhookId = responseObject["webhook_id"];
|
||||
AppSettings().save({
|
||||
'app-webhook-id': responseObject["webhook_id"]
|
||||
}).then((_) {
|
||||
_createNextAlarmSensor(true);
|
||||
completer.complete();
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
popup: Popup(
|
||||
@ -76,7 +91,6 @@ class MobileAppIntegrationManager {
|
||||
}
|
||||
_showError();
|
||||
});
|
||||
return completer.future;
|
||||
} else {
|
||||
Logger.d("App was previously registered. Checking...");
|
||||
var updateData = {
|
||||
@ -84,7 +98,7 @@ class MobileAppIntegrationManager {
|
||||
"data": _appRegistrationData
|
||||
};
|
||||
ConnectionManager().sendHTTPPost(
|
||||
endPoint: "/api/webhook/${ConnectionManager().webhookId}",
|
||||
endPoint: "/api/webhook/${AppSettings().webhookId}",
|
||||
includeAuthHeader: false,
|
||||
data: json.encode(updateData)
|
||||
).then((response) {
|
||||
@ -98,12 +112,8 @@ class MobileAppIntegrationManager {
|
||||
Logger.w("No registration data in response. MobileApp integration was removed or broken");
|
||||
_askToRegisterApp();
|
||||
} else {
|
||||
if (INTEGRATION_VERSION > ConnectionManager().appIntegrationVersion) {
|
||||
Logger.d('App registration needs to be updated');
|
||||
_askToRemoveAndRegisterApp();
|
||||
} else {
|
||||
Logger.d('App registration works fine');
|
||||
}
|
||||
Logger.d('App registration works fine');
|
||||
_createNextAlarmSensor(false);
|
||||
}
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
@ -119,8 +129,44 @@ class MobileAppIntegrationManager {
|
||||
}
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static _createNextAlarmSensor(bool force) {
|
||||
if (AppSettings().nextAlarmSensorCreated && !force) {
|
||||
Logger.d("Next alarm sensor was previously created");
|
||||
return;
|
||||
}
|
||||
Logger.d("Creating next alarm sensor...");
|
||||
ConnectionManager().sendHTTPPost(
|
||||
endPoint: "/api/webhook/${AppSettings().webhookId}",
|
||||
includeAuthHeader: false,
|
||||
data: json.encode(
|
||||
{
|
||||
"data": {
|
||||
"device_class": "timestamp",
|
||||
"icon": "mdi:alarm",
|
||||
"name": "Next Alarm",
|
||||
"state": "",
|
||||
"type": "sensor",
|
||||
"unique_id": "next_alarm"
|
||||
},
|
||||
"type": "register_sensor"
|
||||
}
|
||||
)
|
||||
).then((_){
|
||||
AppSettings().nextAlarmSensorCreated = true;
|
||||
AppSettings().save({
|
||||
'next-alarm-sensor-created': true
|
||||
});
|
||||
}).catchError((e) {
|
||||
if (e is http.Response) {
|
||||
Logger.e("Error creating next alarm sensor: ${e.statusCode}: ${e.body}");
|
||||
} else {
|
||||
Logger.e("Error creating next alarm sensor: ${e?.toString()}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void _showError() {
|
||||
@ -128,28 +174,11 @@ class MobileAppIntegrationManager {
|
||||
popup: Popup(
|
||||
title: "App integration is not working properly",
|
||||
body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue. You can try to remove Mobile App integration from Home Assistant and restart server to fix this issue.",
|
||||
positiveText: "Report to GitHub",
|
||||
negativeText: "Report to Discord",
|
||||
positiveText: "Report issue",
|
||||
negativeText: "Close",
|
||||
onPositive: () {
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
},
|
||||
onNegative: () {
|
||||
Launcher.launchURLInBrowser("https://discord.gg/u9vq7QE");
|
||||
},
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
static void _askToRemoveAndRegisterApp() {
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
popup: Popup(
|
||||
title: "Mobile app integration needs to be updated",
|
||||
body: "You need to update HA Client integration to continue using notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant and restart Home Assistant. Then go back to HA Client to create app integration again.",
|
||||
positiveText: "Ok",
|
||||
negativeText: "Report an issue",
|
||||
onNegative: () {
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
},
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose");
|
||||
}
|
||||
)
|
||||
));
|
||||
}
|
||||
|
@ -11,19 +11,27 @@ class StartupUserMessagesManager {
|
||||
|
||||
StartupUserMessagesManager._internal();
|
||||
|
||||
bool _supportAppDevelopmentMessageShown;
|
||||
bool _needToshowDonateMessage;
|
||||
bool _whatsNewMessageShown;
|
||||
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
|
||||
static final _donateMsgTimerKey = "user-msg-donate-timer";
|
||||
static final _donateMsgShownKey = "user-msg-donate-shpown";
|
||||
static final _whatsNewMessageKey = "user-msg-whats-new-url";
|
||||
|
||||
void checkMessagesToShow() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.reload();
|
||||
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
|
||||
var tInt = prefs.getInt(_donateMsgTimerKey);
|
||||
if (tInt == null) {
|
||||
prefs.setInt(_donateMsgTimerKey, DateTime.now().millisecondsSinceEpoch);
|
||||
_needToshowDonateMessage = false;
|
||||
} else {
|
||||
bool wasShown = prefs.getBool(_donateMsgShownKey) ?? false;
|
||||
_needToshowDonateMessage = (Duration(milliseconds: DateTime.now().millisecondsSinceEpoch - tInt).inDays >= 14) && !wasShown;
|
||||
}
|
||||
_whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl;
|
||||
if (!_whatsNewMessageShown) {
|
||||
_showWhatsNewMessage();
|
||||
} else if (!_supportAppDevelopmentMessageShown) {
|
||||
} else if (_needToshowDonateMessage) {
|
||||
_showSupportAppDevelopmentMessage();
|
||||
}
|
||||
}
|
||||
@ -34,16 +42,16 @@ class StartupUserMessagesManager {
|
||||
title: "Hi!",
|
||||
body: "As you may have noticed this app contains no ads. Also all app features are available for you for free. I'm not planning to change this in nearest future, but still you can support this application development materially. There is one-time payment available as well as several subscription options. Thanks.",
|
||||
positiveText: "Show options",
|
||||
negativeText: "Cancel",
|
||||
negativeText: "Later",
|
||||
onPositive: () {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool(_supportAppDevelopmentMessageKey, true);
|
||||
prefs.setBool(_donateMsgShownKey, true);
|
||||
eventBus.fire(ShowPageEvent(path: "/putchase"));
|
||||
});
|
||||
},
|
||||
onNegative: () {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool(_supportAppDevelopmentMessageKey, true);
|
||||
prefs.setInt(_donateMsgTimerKey, DateTime.now().millisecondsSinceEpoch);
|
||||
});
|
||||
}
|
||||
)
|
||||
|
@ -1,18 +1,37 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class FullScreenPage extends StatelessWidget {
|
||||
class FullScreenPage extends StatefulWidget {
|
||||
|
||||
final Widget child;
|
||||
|
||||
const FullScreenPage({Key key, this.child}) : super(key: key);
|
||||
|
||||
@override
|
||||
_FullScreenPageState createState() => _FullScreenPageState();
|
||||
}
|
||||
|
||||
class _FullScreenPageState extends State<FullScreenPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
SystemChrome.setEnabledSystemUIOverlays([]);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: this.child,
|
||||
child: this.widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
@ -33,35 +33,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
_firebaseMessaging.configure(
|
||||
onLaunch: (data) {
|
||||
Logger.d("Notification [onLaunch]: $data");
|
||||
return Future.value();
|
||||
},
|
||||
onMessage: (data) {
|
||||
Logger.d("Notification [onMessage]: $data");
|
||||
return _showNotification(title: data["notification"]["title"], text: data["notification"]["body"]);
|
||||
},
|
||||
onResume: (data) {
|
||||
Logger.d("Notification [onResume]: $data");
|
||||
return Future.value();
|
||||
}
|
||||
);
|
||||
|
||||
_bottomInfoBarController = BottomInfoBarController();
|
||||
|
||||
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
|
||||
|
||||
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
|
||||
var initializationSettingsAndroid =
|
||||
new AndroidInitializationSettings('mini_icon');
|
||||
var initializationSettingsIOS = new IOSInitializationSettings(
|
||||
onDidReceiveLocalNotification: null);
|
||||
var initializationSettings = new InitializationSettings(
|
||||
initializationSettingsAndroid, initializationSettingsIOS);
|
||||
flutterLocalNotificationsPlugin.initialize(initializationSettings,
|
||||
onSelectNotification: onSelectNotification);
|
||||
|
||||
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
|
||||
Logger.d("Settings change event: reconnect=${event.reconnect}");
|
||||
if (event.reconnect) {
|
||||
@ -73,36 +46,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
_fullLoad();
|
||||
}
|
||||
|
||||
Future onSelectNotification(String payload) async {
|
||||
if (payload != null) {
|
||||
Logger.d('Notification clicked: ' + payload);
|
||||
}
|
||||
}
|
||||
|
||||
Future _showNotification({String title, String text}) async {
|
||||
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
|
||||
'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service',
|
||||
importance: Importance.Max, priority: Priority.High);
|
||||
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
|
||||
var platformChannelSpecifics = new NotificationDetails(
|
||||
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
0,
|
||||
title ?? appName,
|
||||
text,
|
||||
platformChannelSpecifics
|
||||
);
|
||||
}
|
||||
|
||||
void _fullLoad() {
|
||||
_bottomInfoBarController.showInfoBottomBar(progress: true,);
|
||||
Logger.d('[loading] fullLoad');
|
||||
_subscribe().then((_) {
|
||||
Logger.d('[loading] subscribed');
|
||||
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
||||
Logger.d('[loading] COnnection manager initialized');
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
HomeAssistant().lovelaceDashboardUrl = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
|
||||
HomeAssistant().currentDashboardPath = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
|
||||
_fetchData(useCache: true);
|
||||
LocationManager();
|
||||
StartupUserMessagesManager().checkMessagesToShow();
|
||||
MobileAppIntegrationManager.checkAppRegistration();
|
||||
});
|
||||
}, onError: (e) {
|
||||
if (e is HACNotSetUpException) {
|
||||
@ -154,9 +110,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
}
|
||||
}
|
||||
|
||||
Future _subscribe() {
|
||||
Completer completer = Completer();
|
||||
|
||||
Future _subscribe() async {
|
||||
if (_stateSubscription == null) {
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
if (event.needToRebuildUI) {
|
||||
@ -237,16 +191,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
});
|
||||
}
|
||||
|
||||
_firebaseMessaging.getToken().then((String token) {
|
||||
/*_firebaseMessaging.getToken().then((String token) {
|
||||
HomeAssistant().fcmToken = token;
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
});*/
|
||||
}
|
||||
|
||||
void _showOAuth() {
|
||||
_preventAppRefresh = true;
|
||||
Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
|
||||
Navigator.of(context).pushNamed("/auth", arguments: {"url": AppSettings().oauthUrl});
|
||||
}
|
||||
|
||||
_setErrorState(HACException e) {
|
||||
@ -348,7 +301,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
title: Text("Report an issue"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose");
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
@ -449,8 +402,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||
|
||||
Widget _buildScaffoldBody(bool empty) {
|
||||
List<PopupMenuItem<String>> serviceMenuItems = [];
|
||||
List<PopupMenuItem<String>> mediaMenuItems = [];
|
||||
List<Entity> activePlayers = [];
|
||||
List<Entity> activeLights = [];
|
||||
Color mediaMenuIconColor;
|
||||
Color lightMenuIconColor;
|
||||
|
||||
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
||||
if (_previousViewCount != currentViewCount) {
|
||||
@ -459,72 +414,25 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
_previousViewCount = currentViewCount;
|
||||
}
|
||||
|
||||
serviceMenuItems.add(PopupMenuItem<String>(
|
||||
child: new Text("Reload"),
|
||||
value: "reload",
|
||||
));
|
||||
if (ConnectionManager().isAuthenticated) {
|
||||
if (AppSettings().isAuthenticated) {
|
||||
_showLoginButton = false;
|
||||
serviceMenuItems.add(
|
||||
PopupMenuItem<String>(
|
||||
child: new Text("Logout"),
|
||||
value: "logout",
|
||||
));
|
||||
}
|
||||
Widget mediaMenuIcon;
|
||||
int playersCount = 0;
|
||||
|
||||
|
||||
if (!empty && !HomeAssistant().entities.isEmpty) {
|
||||
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
|
||||
playersCount = activePlayers.length;
|
||||
mediaMenuItems.addAll(
|
||||
activePlayers.map((entity) => PopupMenuItem<String>(
|
||||
child: Text(
|
||||
"${entity.displayName}",
|
||||
style: Theme.of(context).textTheme.body1.copyWith(
|
||||
color: HAClientTheme().getColorByEntityState(entity.state, context)
|
||||
)
|
||||
),
|
||||
value: "${entity.entityId}",
|
||||
)).toList()
|
||||
);
|
||||
activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
|
||||
activeLights = HomeAssistant().entities.getByDomains(includeDomains: ["light"], stateFiler: [EntityState.on]);
|
||||
}
|
||||
mediaMenuItems.addAll([
|
||||
PopupMenuItem<String>(
|
||||
child: new Text("Play media..."),
|
||||
value: "play_media",
|
||||
)
|
||||
]);
|
||||
if (playersCount > 0) {
|
||||
mediaMenuIcon = Stack(
|
||||
overflow: Overflow.visible,
|
||||
children: <Widget>[
|
||||
Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:television"), color: Colors.white,),
|
||||
Positioned(
|
||||
bottom: -4,
|
||||
right: -4,
|
||||
child: Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
decoration: new BoxDecoration(
|
||||
color: Colors.orange,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"$playersCount",
|
||||
style: Theme.of(context).textTheme.caption.copyWith(
|
||||
color: Colors.white
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
if (activePlayers.isNotEmpty) {
|
||||
mediaMenuIconColor = Theme.of(context).accentColor;
|
||||
} else {
|
||||
mediaMenuIcon = Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:television"), color: Colors.white,);
|
||||
mediaMenuIconColor = Theme.of(context).primaryIconTheme.color;
|
||||
}
|
||||
if (activeLights.isNotEmpty) {
|
||||
lightMenuIconColor = Theme.of(context).accentColor;
|
||||
} else {
|
||||
lightMenuIconColor = Theme.of(context).primaryIconTheme.color;
|
||||
}
|
||||
Widget mainScrollBody;
|
||||
if (empty) {
|
||||
@ -575,46 +483,43 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
SliverAppBar(
|
||||
floating: true,
|
||||
pinned: true,
|
||||
primary: true,
|
||||
snap: false,
|
||||
primary: AppSettings().displayMode == DisplayMode.normal,
|
||||
title: Text(HomeAssistant().locationName ?? ""),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: mediaMenuIcon,
|
||||
onPressed: () {
|
||||
showMenu(
|
||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, MediaQuery.of(context).padding.top + Sizes.iconSize + 20, 50, 0),
|
||||
context: context,
|
||||
items: mediaMenuItems
|
||||
).then((String val) {
|
||||
if (val == "play_media") {
|
||||
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
|
||||
} else if (val != null) {
|
||||
_showEntityPage(val);
|
||||
}
|
||||
PopupMenuButton(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||
child: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical"), color: Theme.of(context).primaryIconTheme.color)
|
||||
),
|
||||
onSelected: (String val) {
|
||||
if (val == "reload") {
|
||||
_quickLoad();
|
||||
} else if (val == "logout") {
|
||||
HomeAssistant().logout().then((_) {
|
||||
_quickLoad();
|
||||
});
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) {
|
||||
List<PopupMenuEntry<String>> result = [
|
||||
PopupMenuItem<String>(
|
||||
child: new Text("Reload"),
|
||||
value: "reload",
|
||||
)
|
||||
];
|
||||
if (AppSettings().isAuthenticated) {
|
||||
result.addAll([
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<String>(
|
||||
child: new Text("Logout"),
|
||||
value: "logout",
|
||||
)]);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical"), color: Colors.white,),
|
||||
onPressed: () {
|
||||
showMenu(
|
||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, MediaQuery.of(context).padding.top + Sizes.iconSize + 20, 0.0, 0.0),
|
||||
context: context,
|
||||
items: serviceMenuItems
|
||||
).then((String val) {
|
||||
HomeAssistant().lovelaceDashboardUrl = HomeAssistant.DEFAULT_DASHBOARD;
|
||||
if (val == "reload") {
|
||||
|
||||
_quickLoad();
|
||||
} else if (val == "logout") {
|
||||
HomeAssistant().logout().then((_) {
|
||||
_quickLoad();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
],
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.menu),
|
||||
@ -622,6 +527,99 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
_scaffoldKey.currentState.openDrawer();
|
||||
},
|
||||
),
|
||||
expandedHeight: 130,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
title: Padding(
|
||||
padding: EdgeInsets.only(bottom: 15),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
PopupMenuButton<String>(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
|
||||
child: Icon(
|
||||
MaterialDesignIcons.getIconDataFromIconName("mdi:television"),
|
||||
color: mediaMenuIconColor,
|
||||
size: 20,
|
||||
)
|
||||
),
|
||||
onSelected: (String val) {
|
||||
if (val == "play_media") {
|
||||
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
|
||||
} else if (val != null) {
|
||||
_showEntityPage(val);
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) {
|
||||
List<PopupMenuEntry<String>> result = [
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<String>(
|
||||
child: new Text("Play media..."),
|
||||
value: "play_media",
|
||||
)
|
||||
];
|
||||
if (activePlayers.isNotEmpty) {
|
||||
result.insertAll(0,
|
||||
activePlayers.map((entity) => PopupMenuItem<String>(
|
||||
child: Text(
|
||||
"${entity.displayName}",
|
||||
style: Theme.of(context).textTheme.body1.copyWith(
|
||||
color: HAClientTheme().getColorByEntityState(entity.state, context)
|
||||
)
|
||||
),
|
||||
value: "${entity.entityId}",
|
||||
)).toList()
|
||||
);
|
||||
} else {
|
||||
result.insert(0, PopupMenuItem<String>(
|
||||
child: new Text("No active players"),
|
||||
value: "_",
|
||||
enabled: false,
|
||||
));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
|
||||
child: Icon(
|
||||
MaterialDesignIcons.getIconDataFromIconName("mdi:lightbulb-outline"),
|
||||
color: lightMenuIconColor,
|
||||
size: 20
|
||||
)
|
||||
),
|
||||
onSelected: (String val) {
|
||||
if (val == 'turn_off_all') {
|
||||
ConnectionManager().callService(
|
||||
service: 'turn_off',
|
||||
domain: 'light',
|
||||
entityId: 'all'
|
||||
);
|
||||
} else if (val == 'turn_on_all') {
|
||||
ConnectionManager().callService(
|
||||
service: 'turn_on',
|
||||
domain: 'light',
|
||||
entityId: 'all'
|
||||
);
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
child: new Text("Turn on all lights"),
|
||||
value: "turn_on_all",
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
child: new Text("Turn off all ligts"),
|
||||
value: "turn_off_all",
|
||||
enabled: activeLights.isNotEmpty,
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
) ),
|
||||
centerTitle: true,
|
||||
),
|
||||
bottom: empty ? null : TabBar(
|
||||
controller: _viewsTabController,
|
||||
tabs: buildUIViewTabs(),
|
||||
@ -665,6 +663,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
Hive.close();
|
||||
//final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||
//flutterWebviewPlugin.dispose();
|
||||
_viewsTabController?.dispose();
|
||||
|
@ -31,7 +31,7 @@ class _PurchasePageState extends State<PurchasePage> {
|
||||
} else {
|
||||
const Set<String> _kIds = {'one_time_support','just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
|
||||
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
|
||||
if (!response.notFoundIDs.isEmpty) {
|
||||
if (response.notFoundIDs.isNotEmpty) {
|
||||
Logger.d("Products not found: ${response.notFoundIDs}");
|
||||
}
|
||||
_products = response.productDetails;
|
||||
@ -63,7 +63,18 @@ class _PurchasePageState extends State<PurchasePage> {
|
||||
}
|
||||
|
||||
List<Widget> _buildProducts() {
|
||||
List<Widget> productWidgets = [];
|
||||
List<Widget> productWidgets = [
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(15),
|
||||
child: Text(
|
||||
'This will not unlock any additional functionality. This is only a donation to the HA Client open source project.',
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
)
|
||||
)
|
||||
];
|
||||
for (ProductDetails product in _products) {
|
||||
productWidgets.add(
|
||||
ProductPurchase(
|
||||
@ -90,22 +101,6 @@ class _PurchasePageState extends State<PurchasePage> {
|
||||
} else {
|
||||
body = _buildProducts();
|
||||
}
|
||||
body.add(
|
||||
Card(
|
||||
child: Container(
|
||||
height: 80,
|
||||
child: InkWell(
|
||||
child: Image.network('https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif'),
|
||||
onTap: () {
|
||||
Launcher.launchURLInCustomTab(
|
||||
context: context,
|
||||
url: 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ARWGETZD2D83Q&source=url'
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
|
@ -23,7 +23,7 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
|
||||
|
||||
Widget _buildMenuItem(BuildContext context, IconData icon,String title, AppSettingsSection section) {
|
||||
return ListTile(
|
||||
title: Text(title, style: Theme.of(context).textTheme.subhead),
|
||||
title: Text(title),
|
||||
leading: Icon(icon),
|
||||
trailing: Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () {
|
||||
@ -39,7 +39,7 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
|
||||
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings),
|
||||
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel),
|
||||
];
|
||||
if (ConnectionManager().webhookId != null) {
|
||||
if (AppSettings().webhookId != null) {
|
||||
items.insert(1, _buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings));
|
||||
}
|
||||
return ListView(
|
||||
|
@ -31,7 +31,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
}
|
||||
|
||||
_loadSettings() async {
|
||||
_includeDeviceName = widget.quickStart || ConnectionManager().webhookId == null;
|
||||
_includeDeviceName = widget.quickStart || AppSettings().webhookId == null;
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
String domain = prefs.getString('hassio-domain')?? '';
|
||||
String port = prefs.getString('hassio-port') ?? '';
|
||||
|
@ -11,7 +11,7 @@ class IntegrationSettingsPage extends StatefulWidget {
|
||||
|
||||
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
|
||||
int _locationInterval = LocationManager().defaultUpdateIntervalMinutes;
|
||||
int _locationInterval = AppSettings().defaultLocationUpdateIntervalMinutes;
|
||||
bool _locationTrackingEnabled = false;
|
||||
bool _wait = false;
|
||||
|
||||
@ -28,7 +28,8 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
setState(() {
|
||||
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||
_locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
|
||||
_locationInterval = prefs.getInt("location-interval") ??
|
||||
AppSettings().defaultLocationUpdateIntervalMinutes;
|
||||
if (_locationInterval % 5 != 0) {
|
||||
_locationInterval = 5 * (_locationInterval ~/ 5);
|
||||
}
|
||||
|
@ -12,7 +12,8 @@ class LookAndFeelSettingsPage extends StatefulWidget {
|
||||
class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
||||
|
||||
AppTheme _currentTheme;
|
||||
bool _changed = false;
|
||||
bool _scrollBadges = false;
|
||||
DisplayMode _displayMode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -25,14 +26,17 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
||||
await prefs.reload();
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
setState(() {
|
||||
_currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme.index];
|
||||
_currentTheme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
|
||||
_displayMode = DisplayMode.values[prefs.getInt('display-mode') ?? DisplayMode.normal.index];
|
||||
_scrollBadges = prefs.getBool('scroll-badges') ?? true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_saveSettings(AppTheme theme) {
|
||||
_saveTheme(AppTheme theme) {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setInt('app-theme', theme.index);
|
||||
prefs.setBool('scroll-badges', _scrollBadges);
|
||||
setState(() {
|
||||
_currentTheme = theme;
|
||||
eventBus.fire(ChangeThemeEvent(_currentTheme));
|
||||
@ -40,12 +44,34 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Future _saveBadgesSettings() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
AppSettings().scrollBadges = _scrollBadges;
|
||||
await prefs.setBool('scroll-badges', _scrollBadges);
|
||||
}
|
||||
|
||||
Future _saveDisplayMode(DisplayMode mode) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
AppSettings().displayMode = mode;
|
||||
await prefs.setInt('display-mode', mode.index);
|
||||
if (mode == DisplayMode.fullscreen) {
|
||||
SystemChrome.setEnabledSystemUIOverlays([]);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||
}
|
||||
}
|
||||
|
||||
Map appThemeName = {
|
||||
AppTheme.defaultTheme: 'Default',
|
||||
AppTheme.haTheme: 'Home Assistant theme',
|
||||
AppTheme.darkTheme: 'Dark theme'
|
||||
};
|
||||
|
||||
Map DisplayModeName = {
|
||||
DisplayMode.normal: 'Normal',
|
||||
DisplayMode.fullscreen: 'Fullscreen'
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
@ -59,15 +85,56 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
||||
iconSize: 30.0,
|
||||
isExpanded: true,
|
||||
style: Theme.of(context).textTheme.title,
|
||||
//hint: Text("Select ${caption.toLowerCase()}"),
|
||||
items: AppTheme.values.map((value) {
|
||||
return new DropdownMenuItem<AppTheme>(
|
||||
value: value,
|
||||
child: Text('${appThemeName[value]}'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (theme) => _saveSettings(theme),
|
||||
)
|
||||
onChanged: (theme) => _saveTheme(theme),
|
||||
),
|
||||
Container(height: Sizes.doubleRowPadding),
|
||||
Text("Badges display:", style: Theme.of(context).textTheme.body2),
|
||||
Container(height: Sizes.rowPadding),
|
||||
DropdownButton<bool>(
|
||||
value: _scrollBadges,
|
||||
iconSize: 30.0,
|
||||
isExpanded: true,
|
||||
style: Theme.of(context).textTheme.title,
|
||||
items: [true, false].map((value) {
|
||||
return new DropdownMenuItem<bool>(
|
||||
value: value,
|
||||
child: Text('${value ? 'Horizontal scroll' : 'In rows'}'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_scrollBadges = val;
|
||||
});
|
||||
_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 buttonTextInactive = '';
|
||||
if (product.id.contains("year")) {
|
||||
period += "/ year";
|
||||
period += "once a year";
|
||||
buttonText = "Subscribe";
|
||||
buttonTextInactive = "Already";
|
||||
priceColor = Colors.amber;
|
||||
|
@ -20,7 +20,7 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
children: [
|
||||
LinkToWebConfig(name: "Home Assistant configuration", url: ConnectionManager().httpWebHost+"/config"),
|
||||
LinkToWebConfig(name: "Home Assistant configuration", url: AppSettings().httpWebHost+"/config"),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -31,14 +31,15 @@ class Panel {
|
||||
if (componentName.startsWith('haclient')) {
|
||||
Navigator.of(context).pushNamed(urlPath);
|
||||
} else if (componentName == 'lovelace') {
|
||||
HomeAssistant().lovelaceDashboardUrl = this.urlPath;
|
||||
HomeAssistant().currentDashboardPath = this.urlPath;
|
||||
Logger.d('Switching to dashboard: ${this.urlPath}');
|
||||
HomeAssistant().autoUi = false;
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setString('lovelace_dashboard_url', this.urlPath);
|
||||
eventBus.fire(ReloadUIEvent());
|
||||
});
|
||||
} else {
|
||||
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}");
|
||||
Launcher.launchAuthenticatedWebView(context: context, url: "${AppSettings().httpWebHost}/$urlPath", title: "${this.title}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,11 +73,9 @@ class TokenLoginPopup extends Popup {
|
||||
padding: EdgeInsets.all(20),
|
||||
child: TextFormField(
|
||||
onSaved: (newValue) {
|
||||
final storage = new FlutterSecureStorage();
|
||||
storage.write(key: "hacl_llt", value: newValue.trim()).then((_) {
|
||||
Navigator.of(context).pop();
|
||||
eventBus.fire(SettingsChangedEvent(true));
|
||||
});
|
||||
Hive.box(AppSettings.DEFAULT_HIVE_BOX).put(AppSettings.AUTH_TOKEN_KEY, newValue.trim());
|
||||
Navigator.of(context).pop();
|
||||
eventBus.fire(SettingsChangedEvent(true));
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Please enter long-lived token',
|
||||
@ -153,14 +151,14 @@ class RegisterAppPopup extends Popup {
|
||||
Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: TextFormField(
|
||||
initialValue: ConnectionManager().mobileAppDeviceName ?? MobileAppIntegrationManager.getDefaultDeviceName(),
|
||||
initialValue: AppSettings().mobileAppDeviceName ?? MobileAppIntegrationManager.getDefaultDeviceName(),
|
||||
onSaved: (newValue) {
|
||||
String deviceName = newValue?.trim();
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.remove("app-webhook-id");
|
||||
prefs.setString('app-integration-device-name', deviceName);
|
||||
ConnectionManager().webhookId = null;
|
||||
ConnectionManager().mobileAppDeviceName = deviceName;
|
||||
AppSettings().webhookId = null;
|
||||
AppSettings().mobileAppDeviceName = deviceName;
|
||||
Navigator.of(context).pop();
|
||||
MobileAppIntegrationManager.checkAppRegistration();
|
||||
});
|
||||
|
49
lib/ui.dart
49
lib/ui.dart
@ -16,7 +16,7 @@ class HomeAssistantUI {
|
||||
int viewCounter = 0;
|
||||
Logger.d("--Views count: ${rawLovelaceConfig['views'].length}");
|
||||
rawLovelaceConfig["views"].forEach((rawView){
|
||||
Logger.d("----view id: ${rawView['id']}");
|
||||
Logger.d("----view: ${rawView['path'] ?? viewCounter}");
|
||||
HAView view = HAView(
|
||||
count: viewCounter,
|
||||
rawData: rawView
|
||||
@ -30,8 +30,42 @@ class HomeAssistantUI {
|
||||
}
|
||||
|
||||
Map _generateLovelaceConfig() {
|
||||
Map result = {};
|
||||
result['title'] = 'Home';
|
||||
Map result = {
|
||||
'title': 'Home'
|
||||
};
|
||||
List<Entity> left = HomeAssistant().entities.getByDomains(
|
||||
excludeDomains: ['sensor','binary_sensor', 'device_tracker', 'person', 'sun']
|
||||
);
|
||||
List<Map> cards = [];
|
||||
Map<String, Map> cardsByDomains = {};
|
||||
left.forEach((Entity entity) {
|
||||
if (entity is GroupEntity) {
|
||||
cards.add({
|
||||
'type': CardType.ENTITIES,
|
||||
'title': entity.displayName,
|
||||
'entities': entity.childEntities.map((e) => e.entityId)
|
||||
});
|
||||
} else if (entity is MediaPlayerEntity) {
|
||||
cards.add({
|
||||
'type': CardType.MEDIA_CONTROL,
|
||||
'entity': entity.entityId
|
||||
});
|
||||
} else if (entity is AlarmControlPanelEntity) {
|
||||
cards.add({
|
||||
'type': CardType.ALARM_PANEL,
|
||||
'entity': entity.entityId
|
||||
});
|
||||
} else if (cardsByDomains.containsKey(entity.domain)) {
|
||||
cardsByDomains[entity.domain]['entities'].add(entity.entityId);
|
||||
} else {
|
||||
cardsByDomains[entity.domain] = {
|
||||
'type': 'entities',
|
||||
'entities': [entity.entityId],
|
||||
'title': entity.domain
|
||||
};
|
||||
}
|
||||
});
|
||||
cards.addAll(cardsByDomains.values);
|
||||
result['views'] = [
|
||||
{
|
||||
'icon': 'mdi:home',
|
||||
@ -40,14 +74,7 @@ class HomeAssistantUI {
|
||||
).map(
|
||||
(en) => en.entityId
|
||||
).toList(),
|
||||
'cards': [{
|
||||
'type': 'entities',
|
||||
'entities': HomeAssistant().entities.getByDomains(
|
||||
excludeDomains: ['sensor','binary_sensor', 'device_tracker', 'person', 'sun']
|
||||
).map(
|
||||
(en) => en.entityId
|
||||
).toList()
|
||||
}]
|
||||
'cards': cards
|
||||
}
|
||||
];
|
||||
return result;
|
||||
|
@ -45,7 +45,7 @@ class Launcher {
|
||||
if (viewState.type == standaloneWebview.WebViewState.startLoad) {
|
||||
Logger.d("[WebView] Injecting external auth JS");
|
||||
rootBundle.loadString('assets/js/externalAuth.js').then((js){
|
||||
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token));
|
||||
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", AppSettings().longLivedToken));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -2,10 +2,11 @@ part of 'main.dart';
|
||||
|
||||
class HAView {
|
||||
List<CardData> cards = [];
|
||||
List<Entity> badges = [];
|
||||
CardData badges;
|
||||
Entity linkedEntity;
|
||||
String name;
|
||||
String id;
|
||||
String path;
|
||||
String iconName;
|
||||
final int count;
|
||||
bool isPanel;
|
||||
@ -15,29 +16,24 @@ class HAView {
|
||||
name = rawData['title'];
|
||||
iconName = rawData['icon'];
|
||||
isPanel = rawData['panel'] ?? false;
|
||||
path = rawData['path'] ?? '$count';
|
||||
|
||||
if (rawData['badges'] != null && rawData['badges'] is List) {
|
||||
rawData['badges'].forEach((entity) {
|
||||
if (entity is String) {
|
||||
if (HomeAssistant().entities.isExist(entity)) {
|
||||
Entity e = HomeAssistant().entities.get(entity);
|
||||
badges.add(e);
|
||||
}
|
||||
} else {
|
||||
String eId = '${entity['entity']}';
|
||||
if (HomeAssistant().entities.isExist(eId)) {
|
||||
Entity e = HomeAssistant().entities.get(eId);
|
||||
badges.add(e);
|
||||
}
|
||||
}
|
||||
if (rawData['badges'] != null && !isPanel) {
|
||||
badges = CardData.parse({
|
||||
'type': CardType.BADGES,
|
||||
'badges': rawData['badges']
|
||||
});
|
||||
}
|
||||
|
||||
(rawData["cards"] ?? []).forEach((rawCardData) {
|
||||
cards.add(CardData.parse(rawCardData));
|
||||
(rawData['cards'] ?? []).forEach((rawCardData) {
|
||||
if (rawCardData != null) {
|
||||
if (rawCardData is Map) {
|
||||
cards.add(CardData.parse(rawCardData));
|
||||
} else if (rawCardData is List && rawCardData.length == 1) {
|
||||
cards.add(CardData.parse(rawCardData[0]));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//cards.addAll(_createLovelaceCards(rawData["cards"] ?? [], 1));
|
||||
}
|
||||
|
||||
Widget buildTab() {
|
||||
@ -81,4 +77,4 @@ class HAView {
|
||||
view: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,14 +10,17 @@ class ViewWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget cardsContainer;
|
||||
Widget badgesContainer;
|
||||
if (this.view.isPanel) {
|
||||
return FractionallySizedBox(
|
||||
widthFactor: 1,
|
||||
heightFactor: 1,
|
||||
child: _buildPanelChild(context),
|
||||
);
|
||||
cardsContainer = _buildPanelChild(context);
|
||||
badgesContainer = Container(width: 0, height: 0);
|
||||
} else {
|
||||
Widget cardsContainer;
|
||||
if (this.view.badges != null && this.view.badges is BadgesData) {
|
||||
badgesContainer = this.view.badges.buildCardWidget();
|
||||
} else {
|
||||
badgesContainer = Container(width: 0, height: 0);
|
||||
}
|
||||
if (this.view.cards.isNotEmpty) {
|
||||
cardsContainer = DynamicMultiColumnLayout(
|
||||
minColumnWidth: Sizes.minViewColumnWidth,
|
||||
@ -44,15 +47,17 @@ class ViewWidget extends StatelessWidget {
|
||||
} else {
|
||||
cardsContainer = Container();
|
||||
}
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.all(0),
|
||||
children: <Widget>[
|
||||
_buildBadges(context),
|
||||
cardsContainer
|
||||
]
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.all(0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
badgesContainer,
|
||||
cardsContainer
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPanelChild(BuildContext context) {
|
||||
@ -63,18 +68,4 @@ class ViewWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildBadges(BuildContext context) {
|
||||
if (this.view.badges.isNotEmpty) {
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10.0,
|
||||
runSpacing: 1.0,
|
||||
children: this.view.badges.map((badge) =>
|
||||
badge.buildBadgeWidget(context)).toList(),
|
||||
);
|
||||
} else {
|
||||
return Container(width: 0, height: 0,);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
17
pubspec.yaml
17
pubspec.yaml
@ -1,7 +1,7 @@
|
||||
name: hass_client
|
||||
description: Home Assistant Android Client
|
||||
|
||||
version: 1.0.1+1013
|
||||
version: 1.1.2+1161
|
||||
|
||||
|
||||
environment:
|
||||
@ -14,25 +14,24 @@ dependencies:
|
||||
shared_preferences: ^0.5.6+1
|
||||
path_provider: ^1.6.5
|
||||
event_bus: ^1.1.1
|
||||
cached_network_image: ^2.0.0
|
||||
cached_network_image: ^2.3.0-beta
|
||||
url_launcher: ^5.4.1
|
||||
date_format: ^1.0.8
|
||||
charts_flutter: ^0.8.1
|
||||
flutter_markdown: ^0.3.3
|
||||
in_app_purchase: ^0.3.0+3
|
||||
in_app_purchase: ^0.3.4
|
||||
flutter_custom_tabs: ^0.6.0
|
||||
flutter_webview_plugin: ^0.3.10+1
|
||||
webview_flutter: ^0.3.19+7
|
||||
firebase_messaging: ^6.0.13
|
||||
flutter_secure_storage: ^3.3.3
|
||||
device_info: ^0.4.1+4
|
||||
flutter_local_notifications: ^1.1.6
|
||||
hive: ^1.4.1+1
|
||||
hive_flutter: ^0.3.0+2
|
||||
device_info: ^0.4.2+4
|
||||
geolocator: ^5.3.1
|
||||
workmanager: ^0.2.2
|
||||
battery: ^1.0.0
|
||||
firebase_crashlytics: ^0.1.3+3
|
||||
syncfusion_flutter_core: ^18.1.43
|
||||
syncfusion_flutter_gauges: ^18.1.43
|
||||
syncfusion_flutter_core: ^18.1.52
|
||||
syncfusion_flutter_gauges: ^18.1.52
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
|
Reference in New Issue
Block a user