Compare commits

..

1 Commits

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

View File

@ -1,18 +1,18 @@
# HA Client # HA Client
## Native Android client for Home Assistant ## Native Android client for Home Assistant
### With actionable notifications, location tracking and Lovelace UI support ### With notifications and Lovelace UI support
Visit [ha-client.app](http://ha-client.app/) for more info. Visit [ha-client.app](http://ha-client.app/) for more info.
Download the app from [Google Play](https://play.google.com/store/apps/details?id=com.keyboardcrumbs.haclient) Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android) Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/estevez-dev/ha_client) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
#### Last release build status #### Stable CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5eaef46c513b9f9b25eb7f1a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5eaef46c513b9f9b25eb7f1a/latest_build)
#### Beta CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build) [![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
#### Pre-release CI build
#### Special thanks to [![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
- [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/)

View File

@ -79,8 +79,6 @@ flutter {
dependencies { dependencies {
implementation 'com.google.firebase:firebase-analytics:17.2.2' implementation 'com.google.firebase:firebase-analytics:17.2.2'
implementation 'com.google.firebase:firebase-messaging:20.2.0'
implementation 'androidx.work:work-runtime:2.3.4'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

View File

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

View File

@ -5,67 +5,11 @@ import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant; import io.flutter.plugins.GeneratedPluginRegistrant;
import android.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 { public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native";
@Override @Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine); GeneratedPluginRegistrant.registerWith(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(
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);
} }
} }

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 B

View File

@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1g org.gradle.jvmargs=-Xmx2g
org.gradle.daemon=true org.gradle.daemon=true
org.gradle.caching=true org.gradle.caching=true
android.useAndroidX=true android.useAndroidX=true

View File

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

View File

@ -35,19 +35,8 @@ class CardData {
case CardType.ALARM_PANEL: case CardType.ALARM_PANEL:
return AlarmPanelCardData(rawData); return AlarmPanelCardData(rawData);
break; break;
case CardType.LIGHT:
return LightCardData(rawData);
break;
case CardType.PICTURE_ELEMENTS:
//TODO temporary solution
if (rawData.containsKey('camera_image')) {
rawData['entity'] = rawData['camera_image'];
return ButtonCardData(rawData);
} else {
return CardData(null);
}
break;
case CardType.ENTITY_BUTTON: case CardType.ENTITY_BUTTON:
case CardType.LIGHT:
case CardType.BUTTON: case CardType.BUTTON:
case CardType.PICTURE_ENTITY: case CardType.PICTURE_ENTITY:
return ButtonCardData(rawData); return ButtonCardData(rawData);
@ -90,12 +79,6 @@ class CardData {
return BadgesData(rawData); return BadgesData(rawData);
break; break;
default: default:
if (rawData.containsKey('entity')) {
rawData['entities'] = [rawData['entity']];
}
if (rawData.containsKey('entities') && rawData['entities'] is List) {
return EntitiesCardData(rawData);
}
return CardData(null); return CardData(null);
} }
} catch (error, stacktrace) { } catch (error, stacktrace) {
@ -109,11 +92,7 @@ class CardData {
type = rawData['type']; type = rawData['type'];
conditions = rawData['conditions'] ?? []; conditions = rawData['conditions'] ?? [];
showEmpty = rawData['show_empty'] ?? true; showEmpty = rawData['show_empty'] ?? true;
if (rawData.containsKey('state_filter') && rawData['state_filter'] is List) { stateFilter = rawData['state_filter'] ?? [];
stateFilter = rawData['state_filter'];
} else {
stateFilter = [];
}
} else { } else {
type = CardType.UNKNOWN; type = CardType.UNKNOWN;
conditions = []; conditions = [];
@ -216,7 +195,7 @@ class BadgesData extends CardData {
entities.add( entities.add(
EntityWrapper( EntityWrapper(
entity: HomeAssistant().entities.get(rawBadge['entity']), entity: HomeAssistant().entities.get(rawBadge['entity']),
overrideName: rawBadge["name"]?.toString(), overrideName: rawBadge["name"],
overrideIcon: rawBadge["icon"], overrideIcon: rawBadge["icon"],
) )
); );
@ -242,7 +221,7 @@ class BadgesData extends CardData {
entities.add( entities.add(
EntityWrapper( EntityWrapper(
entity: e, entity: e,
overrideName: rawEntity["name"]?.toString(), overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"], overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []), stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []),
uiAction: EntityUIAction(rawEntityData: rawEntity) uiAction: EntityUIAction(rawEntityData: rawEntity)
@ -267,7 +246,7 @@ class EntitiesCardData extends CardData {
EntitiesCardData(rawData) : super(rawData) { EntitiesCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
title = rawData['title']?.toString(); title = rawData['title'];
icon = rawData['icon'] is String ? rawData['icon'] : null; icon = rawData['icon'] is String ? rawData['icon'] : null;
stateColor = rawData['state_color'] ?? false; stateColor = rawData['state_color'] ?? false;
showHeaderToggle = rawData['show_header_toggle'] ?? false; showHeaderToggle = rawData['show_header_toggle'] ?? false;
@ -298,7 +277,7 @@ class EntitiesCardData extends CardData {
EntityWrapper( EntityWrapper(
entity: Entity.callService( entity: Entity.callService(
icon: rawEntity["icon"], icon: rawEntity["icon"],
name: rawEntity["name"]?.toString(), name: rawEntity["name"],
service: rawEntity["service"], service: rawEntity["service"],
actionName: rawEntity["action_name"] actionName: rawEntity["action_name"]
), ),
@ -317,7 +296,7 @@ class EntitiesCardData extends CardData {
entities.add(EntityWrapper( entities.add(EntityWrapper(
entity: Entity.weblink( entity: Entity.weblink(
icon: rawEntity["icon"], icon: rawEntity["icon"],
name: rawEntity["name"]?.toString(), name: rawEntity["name"],
url: rawEntity["url"] url: rawEntity["url"]
), ),
stateColor: rawEntity["state_color"] ?? stateColor, stateColor: rawEntity["state_color"] ?? stateColor,
@ -330,7 +309,7 @@ class EntitiesCardData extends CardData {
EntityWrapper( EntityWrapper(
entity: e, entity: e,
stateColor: rawEntity["state_color"] ?? stateColor, stateColor: rawEntity["state_color"] ?? stateColor,
overrideName: rawEntity["name"]?.toString(), overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"], overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [], stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity) uiAction: EntityUIAction(rawEntityData: rawEntity)
@ -357,7 +336,7 @@ class AlarmPanelCardData extends CardData {
AlarmPanelCardData(rawData) : super(rawData) { AlarmPanelCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
name = rawData['name']?.toString(); name = rawData['name'];
states = rawData['states']; states = rawData['states'];
//Parsing entity //Parsing entity
var entitiId = rawData["entity"]; var entitiId = rawData["entity"];
@ -377,45 +356,6 @@ class AlarmPanelCardData extends CardData {
} }
class LightCardData extends CardData {
String name;
String icon;
@override
Widget buildCardWidget() {
if (this.entity != null && this.entity.entity is LightEntity) {
return LightCard(card: this);
}
return ErrorCard(
errorText: 'Specify an entity from within the light domain.',
showReportButton: false,
);
}
LightCardData(rawData) : super(rawData) {
//Parsing card data
name = rawData['name']?.toString();
icon = rawData['icon'] is String ? rawData['icon'] : null;
//Parsing entity
var entitiId = rawData["entity"];
if (entitiId != null && entitiId is String) {
if (HomeAssistant().entities.isExist(entitiId)) {
entities.add(EntityWrapper(
entity: HomeAssistant().entities.get(entitiId),
overrideName: name,
overrideIcon: icon,
uiAction: EntityUIAction()..tapAction = EntityUIAction.toggle
));
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
} else {
entities.add(EntityWrapper(entity: Entity.missed('$entitiId')));
}
}
}
class ButtonCardData extends CardData { class ButtonCardData extends CardData {
String name; String name;
@ -432,7 +372,7 @@ class ButtonCardData extends CardData {
ButtonCardData(rawData) : super(rawData) { ButtonCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
name = rawData['name']?.toString(); name = rawData['name'];
icon = rawData['icon'] is String ? rawData['icon'] : null; icon = rawData['icon'] is String ? rawData['icon'] : null;
showName = rawData['show_name'] ?? true; showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true; showIcon = rawData['show_icon'] ?? true;
@ -495,7 +435,7 @@ class GaugeCardData extends CardData {
GaugeCardData(rawData) : super(rawData) { GaugeCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
name = rawData['name']?.toString(); name = rawData['name'];
unit = rawData['unit']; unit = rawData['unit'];
if (rawData['min'] is int) { if (rawData['min'] is int) {
min = rawData['min'].toDouble(); min = rawData['min'].toDouble();
@ -547,7 +487,7 @@ class GlanceCardData extends CardData {
GlanceCardData(rawData) : super(rawData) { GlanceCardData(rawData) : super(rawData) {
//Parsing card data //Parsing card data
title = rawData["title"]?.toString(); title = rawData["title"];
showName = rawData['show_name'] ?? true; showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true; showIcon = rawData['show_icon'] ?? true;
showState = rawData['show_state'] ?? true; showState = rawData['show_state'] ?? true;
@ -569,7 +509,7 @@ class GlanceCardData extends CardData {
EntityWrapper( EntityWrapper(
entity: e, entity: e,
stateColor: stateColor, stateColor: stateColor,
overrideName: rawEntity["name"]?.toString(), overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"], overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [], stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity) uiAction: EntityUIAction(rawEntityData: rawEntity)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ class HomeAssistant {
HomeAssistantUI ui; HomeAssistantUI ui;
Map _instanceConfig = {}; Map _instanceConfig = {};
String _userName; String _userName;
String currentDashboardPath; String _lovelaceDashbordUrl;
HSVColor savedColor; HSVColor savedColor;
int savedPlayerPosition; int savedPlayerPosition;
String sendToPlayerId; String sendToPlayerId;
@ -22,11 +22,15 @@ class HomeAssistant {
Map services; Map services;
bool autoUi = false; bool autoUi = false;
String fcmToken;
Map _rawLovelaceData; Map _rawLovelaceData;
var _rawStates; var _rawStates;
var _rawUserInfo; var _rawUserInfo;
var _rawPanels; var _rawPanels;
set lovelaceDashboardUrl(String val) => _lovelaceDashbordUrl = val;
List<Panel> panels = []; List<Panel> panels = [];
Duration fetchTimeout = Duration(seconds: 30); Duration fetchTimeout = Duration(seconds: 30);
@ -59,7 +63,7 @@ class HomeAssistant {
_fetchCompleter = Completer(); _fetchCompleter = Completer();
List<Future> futures = []; List<Future> futures = [];
if (!uiOnly) { if (!uiOnly) {
if (entities == null) entities = EntityCollection(AppSettings().httpWebHost); if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
futures.add(_getStates(null)); futures.add(_getStates(null));
futures.add(_getConfig(null)); futures.add(_getConfig(null));
futures.add(_getUserInfo(null)); futures.add(_getUserInfo(null));
@ -73,6 +77,7 @@ class HomeAssistant {
if (isComponentEnabled('mobile_app')) { if (isComponentEnabled('mobile_app')) {
_createUI(); _createUI();
_fetchCompleter.complete(); _fetchCompleter.complete();
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
} else { } else {
_fetchCompleter.completeError(HACException("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")])); _fetchCompleter.completeError(HACException("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")]));
} }
@ -87,7 +92,7 @@ class HomeAssistant {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
bool cached = prefs.getBool('cached'); bool cached = prefs.getBool('cached');
if (cached != null && cached) { if (cached != null && cached) {
if (entities == null) entities = EntityCollection(AppSettings().httpWebHost); if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
try { try {
_getStates(prefs); _getStates(prefs);
if (!autoUi) { if (!autoUi) {
@ -189,9 +194,9 @@ class HomeAssistant {
} else { } else {
Completer completer = Completer(); Completer completer = Completer();
var additionalData; var additionalData;
if (AppSettings().haVersion >= 107 && currentDashboardPath != HomeAssistant.DEFAULT_DASHBOARD) { if (ConnectionManager().haVersion >= 107 && _lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
additionalData = { additionalData = {
'url_path': currentDashboardPath 'url_path': _lovelaceDashbordUrl
}; };
} }
ConnectionManager().sendSocketMessage( ConnectionManager().sendSocketMessage(
@ -221,7 +226,7 @@ class HomeAssistant {
var data = json.decode(prefs.getString('cached_services')); var data = json.decode(prefs.getString('cached_services'));
_parseServices(data ?? {}); _parseServices(data ?? {});
} catch (e, stacktrace) { } catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true); Logger.e(e, stacktrace: stacktrace);
} }
} }
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) { await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
@ -261,7 +266,7 @@ class HomeAssistant {
var data = json.decode(sharedPrefs.getString('cached_panels')); var data = json.decode(sharedPrefs.getString('cached_panels'));
_parsePanels(data ?? {}); _parsePanels(data ?? {});
} catch (e, stacktrace) { } catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true); Logger.e(e, stacktrace: stacktrace);
panels.clear(); panels.clear();
} }
} else { } else {

View File

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:async'; import 'dart:async';
import 'dart:isolate';
import 'dart:ui';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
@ -16,14 +18,18 @@ import 'package:http/http.dart' as http;
import 'package:charts_flutter/flutter.dart' as charts; import 'package:charts_flutter/flutter.dart' as charts;
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart'; import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:hive/hive.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:device_info/device_info.dart'; import 'package:device_info/device_info.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:in_app_purchase/in_app_purchase.dart';
import 'plugins/dynamic_multi_column_layout.dart'; import 'plugins/dynamic_multi_column_layout.dart';
import 'plugins/spoiler_card.dart'; import 'plugins/spoiler_card.dart';
import 'package:workmanager/workmanager.dart' as workManager; //import 'package:workmanager/workmanager.dart' as workManager;
import 'package:geolocator/geolocator.dart'; //import 'package:geolocator/geolocator.dart';
import 'package:background_locator/background_locator.dart';
import 'package:background_locator/location_dto.dart';
import 'package:background_locator/location_settings.dart';
import 'package:battery/battery.dart'; import 'package:battery/battery.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview; import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
@ -141,7 +147,6 @@ part 'cards/horizontal_srack_card.dart';
part 'cards/markdown_card.dart'; part 'cards/markdown_card.dart';
part 'cards/media_control_card.dart'; part 'cards/media_control_card.dart';
part 'cards/unsupported_card.dart'; part 'cards/unsupported_card.dart';
part 'cards/light_card.dart';
part 'cards/error_card.dart'; part 'cards/error_card.dart';
part 'cards/vertical_stack_card.dart'; part 'cards/vertical_stack_card.dart';
part 'cards/glance_card.dart'; part 'cards/glance_card.dart';
@ -153,14 +158,15 @@ part 'pages/whats_new.page.dart';
part 'pages/fullscreen.page.dart'; part 'pages/fullscreen.page.dart';
part 'popups.dart'; part 'popups.dart';
part 'cards/badges.dart'; part 'cards/badges.dart';
part 'managers/app_settings.dart';
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
//FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = 'HA Client'; const String appName = 'HA Client';
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0'); const appVersionNumber = '1.0.1';
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.1.0-b2.md'; final String appVersionAdd = secrets['version_type'] ?? '';
final String appVersion = '$appVersionNumber${appVersionAdd.isNotEmpty ? '-' : ''}$appVersionAdd';
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.0.1.md';
Future<void> _reportError(dynamic error, dynamic stackTrace) async { Future<void> _reportError(dynamic error, dynamic stackTrace) async {
// Print the exception to the console. // Print the exception to the console.
@ -185,17 +191,12 @@ void main() async {
}; };
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await AppSettings().loadStartupSettings(); SharedPreferences prefs = await SharedPreferences.getInstance();
await Hive.initFlutter(); AppTheme theme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
if (AppSettings().displayMode == DisplayMode.fullscreen) {
SystemChrome.setEnabledSystemUIOverlays([]);
} else {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
}
runZoned(() { runZoned(() {
runApp(new HAClientApp( runApp(new HAClientApp(
theme: AppSettings().appTheme, theme: theme,
)); ));
}, onError: (error, stack) { }, onError: (error, stack) {
_reportError(error, stack); _reportError(error, stack);
@ -218,8 +219,11 @@ class _HAClientAppState extends State<HAClientApp> {
StreamSubscription _themeChangeSubscription; StreamSubscription _themeChangeSubscription;
AppTheme _currentTheme = AppTheme.defaultTheme; AppTheme _currentTheme = AppTheme.defaultTheme;
ReceivePort port = ReceivePort();
@override @override
void initState() { void initState() {
InAppPurchaseConnection.enablePendingPurchases(); InAppPurchaseConnection.enablePendingPurchases();
final Stream purchaseUpdates = final Stream purchaseUpdates =
InAppPurchaseConnection.instance.purchaseUpdatedStream; InAppPurchaseConnection.instance.purchaseUpdatedStream;
@ -232,11 +236,18 @@ class _HAClientAppState extends State<HAClientApp> {
_currentTheme = event.theme; _currentTheme = event.theme;
}); });
}); });
/*
workManager.Workmanager.initialize( workManager.Workmanager.initialize(
updateDeviceLocationIsolate, updateDeviceLocationIsolate,
isInDebugMode: false isInDebugMode: false
); );
*/
super.initState(); super.initState();
IsolateNameServer.registerPortWithName(port.sendPort, LocationManager.isolateName);
port.listen((dynamic data) {
// do something with data
});
initPlatformState();
} }
void _handlePurchaseUpdates(purchase) { void _handlePurchaseUpdates(purchase) {
@ -257,6 +268,11 @@ class _HAClientAppState extends State<HAClientApp> {
} }
} }
Future<void> initPlatformState() async {
await BackgroundLocator.initialize();
}
// This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new MaterialApp( return new MaterialApp(
@ -289,7 +305,7 @@ class _HAClientAppState extends State<HAClientApp> {
"/quick-start": (context) => QuickStartPage(), "/quick-start": (context) => QuickStartPage(),
"/haclient_zha": (context) => ZhaPage(), "/haclient_zha": (context) => ZhaPage(),
"/auth": (context) => new standaloneWebview.WebviewScaffold( "/auth": (context) => new standaloneWebview.WebviewScaffold(
url: "${AppSettings().oauthUrl}", url: "${ConnectionManager().oauthUrl}",
appBar: new AppBar( appBar: new AppBar(
leading: IconButton( leading: IconButton(
icon: Icon(Icons.help), icon: Icon(Icons.help),

View File

@ -1,125 +0,0 @@
part of '../main.dart';
enum DisplayMode {normal, fullscreen}
class AppSettings {
static const DEFAULT_HIVE_BOX = 'defaultSettingsBox';
static const AUTH_TOKEN_KEY = 'llt';
static 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;
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;
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);
}
}

View File

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

View File

@ -13,53 +13,82 @@ class LocationManager {
init(); init();
} }
final int defaultUpdateIntervalMinutes = 20;
final String backgroundTaskId = "haclocationtask0"; final String backgroundTaskId = "haclocationtask0";
final String backgroundTaskTag = "haclocation"; final String backgroundTaskTag = "haclocation";
Duration _updateInterval;
bool _isRunning;
void init() async { void init() async {
if (AppSettings().locationTrackingEnabled) { SharedPreferences prefs = await SharedPreferences.getInstance();
await _startLocationService(); await prefs.reload();
_updateInterval = Duration(minutes: prefs.getInt("location-interval") ??
defaultUpdateIntervalMinutes);
_isRunning = prefs.getBool("location-enabled") ?? false;
if (_isRunning) {
//await _startLocationService();
} }
} }
setSettings(bool enabled, int interval) async { setSettings(bool enabled, int interval) async {
if (interval != AppSettings().locationUpdateInterval.inMinutes) { SharedPreferences prefs = await SharedPreferences.getInstance();
await _stopLocationService(); if (interval != _updateInterval.inMinutes) {
prefs.setInt("location-interval", interval);
_updateInterval = Duration(minutes: interval);
if (_isRunning) {
Logger.d("Stopping location tracking...");
_isRunning = false;
await _stopLocationService();
}
} }
AppSettings().save({ if (enabled && !_isRunning) {
'location-interval': interval,
'location-enabled': enabled
});
AppSettings().locationUpdateInterval = Duration(minutes: interval);
AppSettings().locationTrackingEnabled = enabled;
if (enabled && !AppSettings().locationTrackingEnabled) {
Logger.d("Starting location tracking"); Logger.d("Starting location tracking");
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool("location-enabled", enabled);
_isRunning = true;
await _startLocationService(); await _startLocationService();
} else if (!enabled && AppSettings().locationTrackingEnabled) { } else if (!enabled && _isRunning) {
Logger.d("Stopping location tracking..."); Logger.d("Stopping location tracking...");
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool("location-enabled", enabled);
_isRunning = false;
await _stopLocationService(); await _stopLocationService();
} }
} }
_startLocationService() async { _startLocationService() async {
String webhookId = AppSettings().webhookId; Logger.d('Starting location tracking');
String httpWebHost = AppSettings().httpWebHost; BackgroundLocator.registerLocationUpdate(
locationCallback,
//optional
androidNotificationCallback: locationNotificationCallback,
settings: LocationSettings(
notificationTitle: "HA Client location tracking",
notificationMsg: "HA Client is updating your device location",
wakeLockTime: 20,
autoStop: false,
interval: 10
),
);
/*
String webhookId = ConnectionManager().webhookId;
String httpWebHost = ConnectionManager().httpWebHost;
if (webhookId != null && webhookId.isNotEmpty) { if (webhookId != null && webhookId.isNotEmpty) {
Duration interval; Duration interval;
int delayFactor; int delayFactor;
int taskCount; int taskCount;
Logger.d("Starting location update for every ${AppSettings().locationUpdateInterval Logger.d("Starting location update for every ${_updateInterval
.inMinutes} minutes..."); .inMinutes} minutes...");
if (AppSettings().locationUpdateInterval.inMinutes == 10) { if (_updateInterval.inMinutes == 10) {
interval = Duration(minutes: 20); interval = Duration(minutes: 20);
taskCount = 2; taskCount = 2;
delayFactor = 10; delayFactor = 10;
} else if (AppSettings().locationUpdateInterval.inMinutes == 5) { } else if (_updateInterval.inMinutes == 5) {
interval = Duration(minutes: 15); interval = Duration(minutes: 15);
taskCount = 3; taskCount = 3;
delayFactor = 5; delayFactor = 5;
} else { } else {
interval = AppSettings().locationUpdateInterval; interval = _updateInterval;
taskCount = 1; taskCount = 1;
delayFactor = 0; delayFactor = 0;
} }
@ -85,20 +114,87 @@ class LocationManager {
); );
} }
} }
*/
} }
_stopLocationService() async { _stopLocationService() async {
Logger.d("Canceling previous schedule if any..."); Logger.d('Stopping location tracking');
await workManager.Workmanager.cancelAll(); IsolateNameServer.removePortNameMapping(isolateName);
BackgroundLocator.unRegisterLocationUpdate();
/*Logger.d("Canceling previous schedule if any...");
await workManager.Workmanager.cancelAll();*/
}
static const String isolateName = "HAClientLocatorIsolate";
static void locationCallback(LocationDto locationDto) async {
print('[Background location] Got location: $locationDto');
sendLocationData(locationDto);
final SendPort send = IsolateNameServer.lookupPortByName(isolateName);
send?.send(locationDto);
}
static Future<void> sendLocationData(LocationDto location) async {
print('[Background location] Loading settings...');
SharedPreferences prefs = await SharedPreferences.getInstance();
String domain = prefs.getString('hassio-domain');
String port = prefs.getString('hassio-port');
String webhookId = prefs.getString('app-webhook-id');
String httpWebHost =
"${prefs.getString('hassio-res-protocol')}://$domain:$port";
if (webhookId != null && webhookId.isNotEmpty) {
String url = "$httpWebHost/api/webhook/$webhookId";
Map<String, String> headers = {};
headers["Content-Type"] = "application/json";
Map data = {
"type": "update_location",
"data": {
"gps": [],
"gps_accuracy": 0,
"battery": 100
}
};
try {
if (location.longitude != null && location.latitude != null) {
data["data"]["gps"] = [location.latitude, location.longitude];
data["data"]["gps_accuracy"] = location.accuracy;
print('[Background location] Sending...');
try {
http.Response response = await http.post(
url,
headers: headers,
body: json.encode(data)
);
if (response.statusCode >= 200 && response.statusCode < 300) {
print('[Background location] Success!');
} else {
print('[Background location] Error sending data: ${response.statusCode}');
}
} catch(e) {
print('[Background location] Error sending data: $e');
}
} else {
print('[Background location] Error. Location is null');
}
} catch (e) {
print('[Background location] Error: $e');
}
}
}
static void locationNotificationCallback() {
print('[Background location] User clicked on the notification');
} }
updateDeviceLocation() async { updateDeviceLocation() async {
/*
try { try {
Logger.d("[Foreground location] Started"); Logger.d("[Foreground location] Started");
Geolocator geolocator = Geolocator(); Geolocator geolocator = Geolocator();
var battery = Battery(); var battery = Battery();
String webhookId = AppSettings().webhookId; String webhookId = ConnectionManager().webhookId;
String httpWebHost = AppSettings().httpWebHost; String httpWebHost = ConnectionManager().httpWebHost;
if (webhookId != null && webhookId.isNotEmpty) { if (webhookId != null && webhookId.isNotEmpty) {
Logger.d("[Foreground location] Getting battery level..."); Logger.d("[Foreground location] Getting battery level...");
int batteryLevel = await battery.batteryLevel; int batteryLevel = await battery.batteryLevel;
@ -135,10 +231,12 @@ class LocationManager {
} catch (e, stack) { } catch (e, stack) {
Logger.e('Foreground location error: ${e.toSTring()}', stacktrace: stack); Logger.e('Foreground location error: ${e.toSTring()}', stacktrace: stack);
} }
*/
} }
} }
/*
void updateDeviceLocationIsolate() { void updateDeviceLocationIsolate() {
workManager.Workmanager.executeTask((backgroundTask, data) async { workManager.Workmanager.executeTask((backgroundTask, data) async {
//print("[Background $backgroundTask] Started"); //print("[Background $backgroundTask] Started");
@ -227,3 +325,4 @@ void updateDeviceLocationIsolate() {
return true; return true;
}); });
} }
*/

View File

@ -2,15 +2,17 @@ part of '../main.dart';
class MobileAppIntegrationManager { class MobileAppIntegrationManager {
static const INTEGRATION_VERSION = 3;
static final _appRegistrationData = { static final _appRegistrationData = {
"device_name": "", "device_name": "",
"app_version": "$appVersion", "app_version": "$appVersion",
"manufacturer": DeviceInfoManager().manufacturer ?? "unknown", "manufacturer": DeviceInfoManager().manufacturer,
"model": DeviceInfoManager().model ?? "unknown", "model": DeviceInfoManager().model,
"os_version": DeviceInfoManager().osVersion ?? "0", "os_version": DeviceInfoManager().osVersion,
"app_data": { "app_data": {
"push_token": "", "push_token": "",
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV3" "push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
} }
}; };
@ -21,29 +23,11 @@ class MobileAppIntegrationManager {
return '${HomeAssistant().userName}\'s ${DeviceInfoManager().model}'; return '${HomeAssistant().userName}\'s ${DeviceInfoManager().model}';
} }
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native'); static Future checkAppRegistration() {
static Future checkAppRegistration() async {
String fcmToken = await AppSettings().loadSingle('npush-token');
if (fcmToken != null) {
Logger.d("[MobileAppIntegrationManager] token exist");
await _doCheck(fcmToken);
} else {
Logger.d("[MobileAppIntegrationManager] no fcm token. Requesting...");
try {
fcmToken = await platform.invokeMethod('getFCMToken');
await _doCheck(fcmToken);
} on PlatformException catch (e) {
Logger.e('[MobileAppIntegrationManager] Failed to get FCM token from native: ${e.message}');
}
}
}
static Future _doCheck(String fcmToken) {
Completer completer = Completer(); Completer completer = Completer();
_appRegistrationData["device_name"] = AppSettings().mobileAppDeviceName ?? getDefaultDeviceName(); _appRegistrationData["device_name"] = ConnectionManager().mobileAppDeviceName ?? getDefaultDeviceName();
(_appRegistrationData["app_data"] as Map)["push_token"] = "$fcmToken"; (_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
if (AppSettings().webhookId == null) { if (ConnectionManager().webhookId == null) {
Logger.d("Mobile app was not registered yet. Registering..."); Logger.d("Mobile app was not registered yet. Registering...");
var registrationData = Map.from(_appRegistrationData); var registrationData = Map.from(_appRegistrationData);
registrationData.addAll({ registrationData.addAll({
@ -52,7 +36,7 @@ class MobileAppIntegrationManager {
"os_name": DeviceInfoManager().osName, "os_name": DeviceInfoManager().osName,
"supports_encryption": false, "supports_encryption": false,
}); });
if (AppSettings().haVersion >= 104) { if (ConnectionManager().haVersion >= 104) {
registrationData.addAll({ registrationData.addAll({
"device_id": "${DeviceInfoManager().unicDeviceId}" "device_id": "${DeviceInfoManager().unicDeviceId}"
}); });
@ -64,10 +48,12 @@ class MobileAppIntegrationManager {
).then((response) { ).then((response) {
Logger.d("Processing registration responce..."); Logger.d("Processing registration responce...");
var responseObject = json.decode(response); var responseObject = json.decode(response);
AppSettings().webhookId = responseObject["webhook_id"]; ConnectionManager().webhookId = responseObject["webhook_id"];
AppSettings().save({ ConnectionManager().appIntegrationVersion = INTEGRATION_VERSION;
'app-webhook-id': responseObject["webhook_id"] SharedPreferences.getInstance().then((prefs) {
}).then((prefs) { prefs.setString("app-webhook-id", responseObject["webhook_id"]);
prefs.setInt('app-integration-version', INTEGRATION_VERSION);
completer.complete(); completer.complete();
eventBus.fire(ShowPopupEvent( eventBus.fire(ShowPopupEvent(
popup: Popup( popup: Popup(
@ -90,6 +76,7 @@ class MobileAppIntegrationManager {
} }
_showError(); _showError();
}); });
return completer.future;
} else { } else {
Logger.d("App was previously registered. Checking..."); Logger.d("App was previously registered. Checking...");
var updateData = { var updateData = {
@ -97,7 +84,7 @@ class MobileAppIntegrationManager {
"data": _appRegistrationData "data": _appRegistrationData
}; };
ConnectionManager().sendHTTPPost( ConnectionManager().sendHTTPPost(
endPoint: "/api/webhook/${AppSettings().webhookId}", endPoint: "/api/webhook/${ConnectionManager().webhookId}",
includeAuthHeader: false, includeAuthHeader: false,
data: json.encode(updateData) data: json.encode(updateData)
).then((response) { ).then((response) {
@ -111,7 +98,12 @@ class MobileAppIntegrationManager {
Logger.w("No registration data in response. MobileApp integration was removed or broken"); Logger.w("No registration data in response. MobileApp integration was removed or broken");
_askToRegisterApp(); _askToRegisterApp();
} else { } else {
Logger.d('App registration works fine'); if (INTEGRATION_VERSION > ConnectionManager().appIntegrationVersion) {
Logger.d('App registration needs to be updated');
_askToRemoveAndRegisterApp();
} else {
Logger.d('App registration works fine');
}
} }
completer.complete(); completer.complete();
}).catchError((e) { }).catchError((e) {
@ -127,8 +119,8 @@ class MobileAppIntegrationManager {
} }
completer.complete(); completer.complete();
}); });
return completer.future;
} }
return completer.future;
} }
static void _showError() { static void _showError() {
@ -136,11 +128,28 @@ class MobileAppIntegrationManager {
popup: Popup( popup: Popup(
title: "App integration is not working properly", title: "App integration is not working properly",
body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue. You can try to remove Mobile App integration from Home Assistant and restart server to fix this issue.", body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue. You can try to remove Mobile App integration from Home Assistant and restart server to fix this issue.",
positiveText: "Report issue", positiveText: "Report to GitHub",
negativeText: "Close", negativeText: "Report to Discord",
onPositive: () { onPositive: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose"); Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
} },
onNegative: () {
Launcher.launchURLInBrowser("https://discord.gg/u9vq7QE");
},
)
));
}
static void _askToRemoveAndRegisterApp() {
eventBus.fire(ShowPopupEvent(
popup: Popup(
title: "Mobile app integration needs to be updated",
body: "You need to update HA Client integration to continue using notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant and restart Home Assistant. Then go back to HA Client to create app integration again.",
positiveText: "Ok",
negativeText: "Report an issue",
onNegative: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
},
) )
)); ));
} }

View File

@ -11,27 +11,19 @@ class StartupUserMessagesManager {
StartupUserMessagesManager._internal(); StartupUserMessagesManager._internal();
bool _needToshowDonateMessage; bool _supportAppDevelopmentMessageShown;
bool _whatsNewMessageShown; bool _whatsNewMessageShown;
static final _donateMsgTimerKey = "user-msg-donate-timer"; static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
static final _donateMsgShownKey = "user-msg-donate-shpown";
static final _whatsNewMessageKey = "user-msg-whats-new-url"; static final _whatsNewMessageKey = "user-msg-whats-new-url";
void checkMessagesToShow() async { void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload(); await prefs.reload();
var tInt = prefs.getInt(_donateMsgTimerKey); _supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
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; _whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl;
if (!_whatsNewMessageShown) { if (!_whatsNewMessageShown) {
_showWhatsNewMessage(); _showWhatsNewMessage();
} else if (_needToshowDonateMessage) { } else if (!_supportAppDevelopmentMessageShown) {
_showSupportAppDevelopmentMessage(); _showSupportAppDevelopmentMessage();
} }
} }
@ -42,16 +34,16 @@ class StartupUserMessagesManager {
title: "Hi!", 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.", 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", positiveText: "Show options",
negativeText: "Later", negativeText: "Cancel",
onPositive: () { onPositive: () {
SharedPreferences.getInstance().then((prefs) { SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_donateMsgShownKey, true); prefs.setBool(_supportAppDevelopmentMessageKey, true);
eventBus.fire(ShowPageEvent(path: "/putchase")); eventBus.fire(ShowPageEvent(path: "/putchase"));
}); });
}, },
onNegative: () { onNegative: () {
SharedPreferences.getInstance().then((prefs) { SharedPreferences.getInstance().then((prefs) {
prefs.setInt(_donateMsgTimerKey, DateTime.now().millisecondsSinceEpoch); prefs.setBool(_supportAppDevelopmentMessageKey, true);
}); });
} }
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ class IntegrationSettingsPage extends StatefulWidget {
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> { class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
int _locationInterval = AppSettings().defaultLocationUpdateIntervalMinutes; int _locationInterval = LocationManager().defaultUpdateIntervalMinutes;
bool _locationTrackingEnabled = false; bool _locationTrackingEnabled = false;
bool _wait = false; bool _wait = false;
@ -28,8 +28,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
SharedPreferences.getInstance().then((prefs) { SharedPreferences.getInstance().then((prefs) {
setState(() { setState(() {
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false; _locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
_locationInterval = prefs.getInt("location-interval") ?? _locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
AppSettings().defaultLocationUpdateIntervalMinutes;
if (_locationInterval % 5 != 0) { if (_locationInterval % 5 != 0) {
_locationInterval = 5 * (_locationInterval ~/ 5); _locationInterval = 5 * (_locationInterval ~/ 5);
} }
@ -54,9 +53,11 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
} }
_switchLocationTrackingState(bool state) async { _switchLocationTrackingState(bool state) async {
/*
if (state) { if (state) {
await LocationManager().updateDeviceLocation(); await LocationManager().updateDeviceLocation();
} }
*/
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval); await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
setState(() { setState(() {
_wait = false; _wait = false;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
name: hass_client name: hass_client
description: Home Assistant Android Client description: Home Assistant Android Client
version: 1.1.1+1158 version: 1.1.0+1100
environment: environment:
@ -14,7 +14,7 @@ dependencies:
shared_preferences: ^0.5.6+1 shared_preferences: ^0.5.6+1
path_provider: ^1.6.5 path_provider: ^1.6.5
event_bus: ^1.1.1 event_bus: ^1.1.1
cached_network_image: ^2.3.0-beta cached_network_image: ^2.0.0
url_launcher: ^5.4.1 url_launcher: ^5.4.1
date_format: ^1.0.8 date_format: ^1.0.8
charts_flutter: ^0.8.1 charts_flutter: ^0.8.1
@ -23,15 +23,17 @@ dependencies:
flutter_custom_tabs: ^0.6.0 flutter_custom_tabs: ^0.6.0
flutter_webview_plugin: ^0.3.10+1 flutter_webview_plugin: ^0.3.10+1
webview_flutter: ^0.3.19+7 webview_flutter: ^0.3.19+7
hive: ^1.4.1+1 firebase_messaging: ^6.0.13
hive_flutter: ^0.3.0+2 flutter_secure_storage: ^3.3.3
device_info: ^0.4.2+4 device_info: ^0.4.1+4
geolocator: ^5.3.1 flutter_local_notifications: ^1.1.6
workmanager: ^0.2.2 #geolocator: ^5.3.1
background_locator: ^1.1.3+1
#workmanager: ^0.2.2
battery: ^1.0.0 battery: ^1.0.0
firebase_crashlytics: ^0.1.3+3 firebase_crashlytics: ^0.1.3+3
syncfusion_flutter_core: ^18.1.52 syncfusion_flutter_core: ^18.1.48
syncfusion_flutter_gauges: ^18.1.52 syncfusion_flutter_gauges: ^18.1.48
dev_dependencies: dev_dependencies: