Compare commits
24 Commits
1.1.0-beta
...
rc/1.1.0-b
Author | SHA1 | Date | |
---|---|---|---|
e295a36465 | |||
9a09a83dc6 | |||
95ca80949f | |||
80b5763530 | |||
9a5e35b024 | |||
4493975676 | |||
141a68faf7 | |||
a8efe7dbb6 | |||
9608983994 | |||
8eb15ab9a4 | |||
aac0cfbb56 | |||
343494ece0 | |||
b1e5e73278 | |||
9b5a0068fd | |||
aa26212ddd | |||
1c45f96706 | |||
c2d5192c51 | |||
88ae80507c | |||
55868d1dfe | |||
92a1230267 | |||
d3f99fb262 | |||
3fdf016c39 | |||
8ce0e8aafa | |||
54f6fb28ef |
@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
|
||||
@ -12,3 +12,7 @@ Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant commun
|
||||
|
||||
#### Last release build status
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
|
||||
|
||||
#### Special thanks to
|
||||
- [Crewski](https://github.com/Crewski) for his [HANotify](https://github.com/Crewski/HANotify)
|
||||
- [Home Assistant](https://github.com/home-assistant) for some support and [Home Assistant](https://www.home-assistant.io/)
|
@ -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'
|
||||
|
@ -16,7 +16,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 +42,26 @@
|
||||
<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>
|
||||
|
||||
<service
|
||||
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
|
@ -5,11 +5,57 @@ 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.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) {
|
||||
if (call.method.equals("getFCMToken")) {
|
||||
FirebaseInstanceId.getInstance().getInstanceId()
|
||||
.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
|
||||
@Override
|
||||
public void onComplete(@NonNull Task<InstanceIdResult> task) {
|
||||
if (task.isSuccessful()) {
|
||||
Context context = getActivity();
|
||||
String token = task.getResult().getToken();
|
||||
UpdateTokenTask updateTokenTask = new UpdateTokenTask(context);
|
||||
updateTokenTask.execute(token);
|
||||
result.success(token);
|
||||
} else {
|
||||
result.error("fcm_error", task.getException().getMessage(), task.getException());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@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 (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
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 = "NotificationActionReceiver";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
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,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 |
@ -197,7 +197,7 @@ class BadgesData extends CardData {
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(rawBadge['entity']),
|
||||
overrideName: rawBadge["name"],
|
||||
overrideName: rawBadge["name"]?.toString(),
|
||||
overrideIcon: rawBadge["icon"],
|
||||
)
|
||||
);
|
||||
@ -223,7 +223,7 @@ class BadgesData extends CardData {
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
overrideName: rawEntity["name"],
|
||||
overrideName: rawEntity["name"]?.toString(),
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []),
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
@ -248,7 +248,7 @@ class EntitiesCardData extends CardData {
|
||||
|
||||
EntitiesCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData['title'];
|
||||
title = rawData['title']?.toString();
|
||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||
stateColor = rawData['state_color'] ?? false;
|
||||
showHeaderToggle = rawData['show_header_toggle'] ?? false;
|
||||
@ -279,7 +279,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"]
|
||||
),
|
||||
@ -298,7 +298,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,
|
||||
@ -311,7 +311,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)
|
||||
@ -338,7 +338,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"];
|
||||
@ -370,7 +370,7 @@ class LightCardData extends CardData {
|
||||
|
||||
LightCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name'];
|
||||
name = rawData['name']?.toString();
|
||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||
//Parsing entity
|
||||
var entitiId = rawData["entity"];
|
||||
@ -407,7 +407,7 @@ class ButtonCardData extends CardData {
|
||||
|
||||
ButtonCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name'];
|
||||
name = rawData['name']?.toString();
|
||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||
showName = rawData['show_name'] ?? true;
|
||||
showIcon = rawData['show_icon'] ?? true;
|
||||
@ -470,7 +470,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();
|
||||
@ -522,7 +522,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;
|
||||
@ -544,7 +544,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)
|
||||
|
@ -78,9 +78,21 @@ class Entity {
|
||||
chartType: EntityHistoryWidgetType.simple
|
||||
);
|
||||
|
||||
String get displayName =>
|
||||
attributes["friendly_name"] ??
|
||||
(attributes["name"] ?? (entityId != null && entityId.contains('.')) ? entityId.split(".")[1].replaceAll("_", " ") : "");
|
||||
String get displayName {
|
||||
if (attributes.containsKey('friendly_name')) {
|
||||
return attributes['friendly_name'];
|
||||
}
|
||||
if (attributes.containsKey('name')) {
|
||||
return attributes['name'];
|
||||
}
|
||||
if (entityId == null) {
|
||||
return "";
|
||||
}
|
||||
if (entityId.contains(".")) {
|
||||
return entityId.split(".")[1].replaceAll("_", " ");
|
||||
}
|
||||
return entityId;
|
||||
}
|
||||
|
||||
bool get isView =>
|
||||
(domain == "group") &&
|
||||
|
@ -22,8 +22,6 @@ class HomeAssistant {
|
||||
Map services;
|
||||
bool autoUi = false;
|
||||
|
||||
String fcmToken;
|
||||
|
||||
Map _rawLovelaceData;
|
||||
var _rawStates;
|
||||
var _rawUserInfo;
|
||||
|
@ -16,10 +16,8 @@ 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: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';
|
||||
@ -157,11 +155,11 @@ part 'cards/badges.dart';
|
||||
part 'managers/app_settings.dart';
|
||||
|
||||
EventBus eventBus = new EventBus();
|
||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||
//FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||
const String appName = 'HA Client';
|
||||
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0');
|
||||
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.1.0.md';
|
||||
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.1.0-b2.md';
|
||||
|
||||
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
||||
// Print the exception to the console.
|
||||
|
@ -22,7 +22,6 @@ class AppSettings {
|
||||
String webhookId;
|
||||
double haVersion;
|
||||
bool scrollBadges;
|
||||
int appIntegrationVersion;
|
||||
AppTheme appTheme;
|
||||
final int defaultLocationUpdateIntervalMinutes = 20;
|
||||
Duration locationUpdateInterval;
|
||||
@ -44,7 +43,6 @@ class AppSettings {
|
||||
_port = prefs.getString('hassio-port');
|
||||
webhookId = prefs.getString('app-webhook-id');
|
||||
mobileAppDeviceName = prefs.getString('app-integration-device-name');
|
||||
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
|
||||
scrollBadges = prefs.getBool('scroll-badges') ?? true;
|
||||
displayHostname = "$_domain:$_port";
|
||||
webSocketAPIEndpoint =
|
||||
@ -69,6 +67,11 @@ class AppSettings {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -2,8 +2,6 @@ part of '../main.dart';
|
||||
|
||||
class MobileAppIntegrationManager {
|
||||
|
||||
static const INTEGRATION_VERSION = 3;
|
||||
|
||||
static final _appRegistrationData = {
|
||||
"device_name": "",
|
||||
"app_version": "$appVersion",
|
||||
@ -12,7 +10,7 @@ class MobileAppIntegrationManager {
|
||||
"os_version": DeviceInfoManager().osVersion,
|
||||
"app_data": {
|
||||
"push_token": "",
|
||||
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV2"
|
||||
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV3"
|
||||
}
|
||||
};
|
||||
|
||||
@ -23,10 +21,28 @@ 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"] = AppSettings().mobileAppDeviceName ?? getDefaultDeviceName();
|
||||
(_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
|
||||
(_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);
|
||||
@ -49,10 +65,8 @@ class MobileAppIntegrationManager {
|
||||
Logger.d("Processing registration responce...");
|
||||
var responseObject = json.decode(response);
|
||||
AppSettings().webhookId = responseObject["webhook_id"];
|
||||
AppSettings().appIntegrationVersion = INTEGRATION_VERSION;
|
||||
AppSettings().save({
|
||||
'app-webhook-id': responseObject["webhook_id"],
|
||||
'app-integration-version': INTEGRATION_VERSION
|
||||
'app-webhook-id': responseObject["webhook_id"]
|
||||
}).then((prefs) {
|
||||
completer.complete();
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
@ -76,7 +90,6 @@ class MobileAppIntegrationManager {
|
||||
}
|
||||
_showError();
|
||||
});
|
||||
return completer.future;
|
||||
} else {
|
||||
Logger.d("App was previously registered. Checking...");
|
||||
var updateData = {
|
||||
@ -98,12 +111,7 @@ class MobileAppIntegrationManager {
|
||||
Logger.w("No registration data in response. MobileApp integration was removed or broken");
|
||||
_askToRegisterApp();
|
||||
} else {
|
||||
if (INTEGRATION_VERSION > AppSettings().appIntegrationVersion) {
|
||||
Logger.d('App registration needs to be updated');
|
||||
_askToRemoveAndRegisterApp();
|
||||
} else {
|
||||
Logger.d('App registration works fine');
|
||||
}
|
||||
Logger.d('App registration works fine');
|
||||
}
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
@ -119,8 +127,8 @@ class MobileAppIntegrationManager {
|
||||
}
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static void _showError() {
|
||||
@ -137,20 +145,6 @@ class MobileAppIntegrationManager {
|
||||
));
|
||||
}
|
||||
|
||||
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");
|
||||
},
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
static void _askToRegisterApp() {
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
popup: RegisterAppPopup(
|
||||
|
@ -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,31 +46,13 @@ 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().currentDashboardPath = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
|
||||
_fetchData(useCache: true);
|
||||
@ -155,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) {
|
||||
@ -238,11 +191,10 @@ 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() {
|
||||
|
@ -26,7 +26,13 @@ class HAView {
|
||||
}
|
||||
|
||||
(rawData['cards'] ?? []).forEach((rawCardData) {
|
||||
cards.add(CardData.parse(rawCardData));
|
||||
if (rawCardData != null) {
|
||||
if (rawCardData is Map) {
|
||||
cards.add(CardData.parse(rawCardData));
|
||||
} else if (rawCardData is List && rawCardData.length == 1) {
|
||||
cards.add(CardData.parse(rawCardData[0]));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -71,4 +77,4 @@ class HAView {
|
||||
view: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
name: hass_client
|
||||
description: Home Assistant Android Client
|
||||
|
||||
version: 0.0.0+1144
|
||||
version: 0.0.0+1151
|
||||
|
||||
|
||||
environment:
|
||||
@ -23,10 +23,8 @@ dependencies:
|
||||
flutter_custom_tabs: ^0.6.0
|
||||
flutter_webview_plugin: ^0.3.10+1
|
||||
webview_flutter: ^0.3.19+7
|
||||
firebase_messaging: ^6.0.15
|
||||
flutter_secure_storage: ^3.3.3
|
||||
device_info: ^0.4.1+4
|
||||
flutter_local_notifications: ^1.1.6
|
||||
geolocator: ^5.3.1
|
||||
workmanager: ^0.2.2
|
||||
battery: ^1.0.0
|
||||
|
Reference in New Issue
Block a user