Compare commits

...

71 Commits

Author SHA1 Message Date
66cd7ea307 0.6.0-alpha3 2019-08-16 15:05:43 +03:00
b704ce6984 0.6.0-alpha3 2019-08-16 14:01:10 +03:00
247c856a41 Resolves #397 Add default icon for device_tracker 2019-08-16 13:44:29 +03:00
9afaebfa12 Resolves #401 Climate support fixes 2019-08-16 13:29:41 +03:00
929abea5d3 Login and mobile app registration improvements 2019-08-16 12:32:36 +03:00
5c31ddd00f Resolves #345 Add default icon for Remote 2019-06-23 16:19:28 +03:00
8f55be187d Resolves #324 devider fix, entity card padding fix 2019-06-23 16:08:12 +03:00
1fe82d8b0d Resolves #334 Fix plug device_class icons 2019-06-23 15:27:55 +03:00
cbc56a8105 Resolves #336 Replace 'unknown' state with '-'. Show displayState for badges 2019-06-23 15:24:08 +03:00
b63cddfa46 Resolves #330 Add Help menu item 2019-06-23 15:15:33 +03:00
91db82f730 Resolves #331 Menu item text change 2019-06-23 15:11:04 +03:00
0c4d1b78ff Resolves #323 fix widget padding for entity page 2019-06-23 15:09:18 +03:00
5af2fd0562 Resolves #376 Dynamic font size on badges 2019-06-23 14:53:11 +03:00
2375543ebf Fix camera stream open 2019-06-23 14:36:15 +03:00
de187f3ed5 Update mdi array builder script 2019-06-21 21:30:26 +03:00
9266ffacf3 Update Material Design Icons font to 3.6.95 2019-06-21 21:28:37 +03:00
3c0ca5d16d Resolve #382 VIew camera in chrome custom tab 2019-06-21 21:01:53 +03:00
caabf25260 WIP #382 Open camera stream in CHrome custom tab 2019-06-21 14:29:56 +03:00
0af2afbb80 Add links to web version of COnfiguration secrtions 2019-06-21 13:33:28 +03:00
12d226509d App registration improvements 2019-06-21 13:21:30 +03:00
3417c38426 Resolves #386 2019-06-21 12:53:03 +03:00
c7fc5afbb8 Resolves #389 Improve app registration checking 2019-06-21 12:39:58 +03:00
11f565a9dc Resolves #388 2019-06-21 12:05:55 +03:00
53240faac3 Fix automatic OAuth window open issue 2019-06-16 22:57:50 +03:00
95d4878785 Resolves #48 Native notifications 2019-06-16 20:08:50 +03:00
ef15026203 Fix authentication process. App register in background 2019-06-16 16:32:55 +03:00
ad6355503b WIP #48 Show dialog on app registration 2019-06-16 00:23:11 +03:00
491c2b0dc0 WIP #48 Notifications with mobile_app component 2019-06-16 00:08:13 +03:00
5b99ade088 Resolves #318 add mobile_app integration 2019-06-15 18:07:11 +03:00
e1d9d9f304 Stop connection init if settings is empty 2019-06-15 14:36:11 +03:00
209ccd4f7f New error class 2019-04-19 21:43:52 +03:00
5a8a207f2e minor fix 2019-04-19 14:40:05 +03:00
19c85d9c16 Don't handle state change if fetch is in progress 2019-04-19 14:38:02 +03:00
a916ddfa50 Resolves #364, Resolves #363 Connection issues 2019-04-19 14:07:44 +03:00
8c1ad9c7f9 Fix login button 2019-04-05 14:07:03 +03:00
93af1eca7e Resolves #355 Add login button on empty screen 2019-04-05 13:39:54 +03:00
cabf836fa3 WIP #355 Disconnect when logout 2019-04-05 13:06:14 +03:00
15b3d31a6f Resolves #353 Show error if connection drops 2019-04-05 12:23:31 +03:00
9b98689012 Fix connection error handling 2019-04-05 12:08:32 +03:00
84ebd0c33c Resolves #352 Fix panels clear after logout 2019-04-05 11:59:13 +03:00
ccd7774931 Resolves #350 Fix displayed hostname 2019-04-05 11:57:58 +03:00
b2773635f5 Connection improvements 2019-04-05 11:48:41 +03:00
8b046b7313 Merge branch '0.6.0-alpha1-1' 2019-04-04 22:25:19 +03:00
885a516676 alpha2 2019-04-04 22:12:08 +03:00
921b0e09b0 Merge branch 'terms_and_privacy' into 0.6.0-alpha1-1 2019-04-04 22:10:29 +03:00
277c67fc6f Add padding for links in About dialog 2019-04-04 21:54:41 +03:00
2a01ff8a03 Bump version in UI 2019-04-04 21:51:05 +03:00
b246b7bc1d 0.5.3 and new build numbers 2019-04-04 21:44:16 +03:00
e1868b9a14 Add privacy polici and terms and conditions links 2019-04-04 21:43:23 +03:00
125f3ac16c Resolves #327 Timer duration parsing error 2019-04-04 21:38:23 +03:00
be502b5668 Discord icon fix 2019-04-04 21:38:05 +03:00
6f33fdca9f New app icon 2019-04-04 21:37:41 +03:00
a7cda2a35e WIP #48 Notifications 2019-03-30 00:29:52 +02:00
102b10ade0 WIP #48 Notifications 2019-03-29 13:09:34 +02:00
4e96b9adbb Build 101 2019-03-29 11:16:04 +02:00
b9581d3762 Resolves #347, Resolves #346 Connection and reconnection 2019-03-29 11:04:43 +02:00
7c010359c3 Resolves #340 Connection refactoring 2019-03-26 00:18:30 +02:00
4a75243994 WIP #340 Refactor getting data and error handling 2019-03-22 14:04:20 +02:00
d29d7e5b3b WIP #340 2019-03-21 16:55:25 +02:00
5ebd25e0d1 Resolves #59 Storing token in secure storage 2019-03-21 14:25:05 +02:00
b7d5a53e86 Resolves #341 Add logout 2019-03-21 14:08:07 +02:00
20d3498bfd WIP #341 Logout 2019-03-20 23:38:57 +02:00
67d7bb45f5 Resolves #338 OAuth with Home Assistant 2019-03-20 23:05:25 +02:00
6a03105d01 WIP 2019-03-20 19:01:30 +02:00
5ae580ecf1 Chachesd HomeAssistance instance for every view in app 2019-03-20 12:48:00 +02:00
0efef33e53 Fix CleartextTraffic issue. WIP #338 2019-03-19 23:20:57 +02:00
ccb88884a7 Settings loading refactored. WIP #338 2019-03-19 23:07:40 +02:00
d70ba0a55a WIP #48 2019-03-18 23:37:45 +02:00
5140840d3a Resolves #327 Timer duration parsing error 2019-03-14 16:39:37 +02:00
14759fd3c9 Discord icon fix 2019-03-14 14:35:30 +02:00
fed35be517 New app icon 2019-03-14 14:07:36 +02:00
62 changed files with 1927 additions and 1020 deletions

3
.gitignore vendored
View File

@ -10,4 +10,5 @@ build/
.idea/
key.properties
key.properties
pubspec.lock

View File

@ -70,7 +70,10 @@ flutter {
}
dependencies {
implementation 'com.google.firebase:firebase-core:16.0.8'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
apply plugin: 'com.google.gms.google-services'

View File

@ -0,0 +1,42 @@
{
"project_info": {
"project_number": "441874387819",
"firebase_url": "https://ha-client-c73c4.firebaseio.com",
"project_id": "ha-client-c73c4",
"storage_bucket": "ha-client-c73c4.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:441874387819:android:92c7efc892dc3d45",
"android_client_info": {
"package_name": "com.keyboardcrumbs.haclient"
}
},
"oauth_client": [
{
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBsl9cjBY633IrdrTyCsLFlO9lfsYJ0OJU"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"
}

View File

@ -6,6 +6,7 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE" />
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
@ -15,7 +16,13 @@
<application
android:name="io.flutter.app.FlutterApplication"
android:label="HA Client"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="ha_notify" />
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
@ -26,14 +33,18 @@
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme). -->
defined in @style/LaunchTheme).
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
android:value="true" />-->
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -5,7 +5,8 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.2'
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.google.gms:google-services:4.2.0'
}
}

View File

@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx2g
org.gradle.daemon=true
org.gradle.caching=true
android.useAndroidX=true
android.enableJetifier=false
android.enableJetifier=true

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,46 @@
part of 'main.dart';
class AuthManager {
static final AuthManager _instance = AuthManager._internal();
factory AuthManager() {
return _instance;
}
AuthManager._internal();
Future getTempToken({String oauthUrl}) {
Completer completer = Completer();
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.onUrlChanged.listen((String url) {
Logger.d("Webview url changed to $url");
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) {
String authCode = url.split("=")[1];
Logger.d("We have auth code. Getting temporary access token...");
Connection().sendHTTPPost(
endPoint: "/auth/token",
contentType: "application/x-www-form-urlencoded",
includeAuthHeader: false,
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}"
).then((response) {
Logger.d("Gottemp token");
String tempToken = json.decode(response)['access_token'];
Logger.d("Closing webview...");
//flutterWebviewPlugin.close();
eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.complete(tempToken);
}).catchError((e) {
//flutterWebviewPlugin.close();
Logger.e("Error getting temp token: ${e.toString()}");
eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.completeError(HAError("Error getting temp token"));
});
}
});
Logger.d("Launching OAuth: $oauthUrl");
eventBus.fire(StartAuthEvent(oauthUrl, true));
return completer.future;
}
}

415
lib/connection.class.dart Normal file
View File

@ -0,0 +1,415 @@
part of 'main.dart';
class Connection {
static final Connection _instance = Connection._internal();
factory Connection() {
return _instance;
}
Connection._internal();
String _domain;
String _port;
String displayHostname;
String _webSocketAPIEndpoint;
String httpWebHost;
String _token;
String _tempToken;
String oauthUrl;
String webhookId;
bool useLovelace = true;
bool settingsLoaded = false;
bool get isAuthenticated => _token != null;
StreamSubscription _socketSubscription;
Duration connectTimeout = Duration(seconds: 15);
bool isConnected = false;
var onStateChangeCallback;
IOWebSocketChannel _socket;
int _currentMessageId = 0;
Map<String, Completer> _messageResolver = {};
Future init({bool loadSettings, bool forceReconnect: false}) async {
Completer completer = Completer();
bool stopInit = false;
if (loadSettings) {
Logger.e("Loading settings...");
SharedPreferences prefs = await SharedPreferences.getInstance();
useLovelace = prefs.getBool('use-lovelace') ?? true;
_domain = prefs.getString('hassio-domain');
_port = prefs.getString('hassio-port');
webhookId = prefs.getString('app-webhook-id');
displayHostname = "$_domain:$_port";
_webSocketAPIEndpoint =
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
httpWebHost =
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
if ((_domain == null) || (_port == null) ||
(_domain.isEmpty) || (_port.isEmpty)) {
completer.completeError(HAError.checkConnectionSettings());
stopInit = true;
} else {
//_token = prefs.getString('hassio-token');
final storage = new FlutterSecureStorage();
try {
_token = await storage.read(key: "hacl_llt");
Logger.e("Long-lived token read successful");
} catch (e) {
Logger.e("Cannt read secure storage. Need to relogin.");
_token = null;
await storage.delete(key: "hacl_llt");
}
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
.encodeComponent(
'http://ha-client.homemade.systems/service/auth_callback.html')}";
settingsLoaded = true;
}
} else {
if ((_domain == null) || (_port == null) ||
(_domain.isEmpty) || (_port.isEmpty)) {
completer.completeError(HAError.checkConnectionSettings());
stopInit = true;
}
}
if (!stopInit) {
if (_token == null) {
AuthManager().getTempToken(
oauthUrl: oauthUrl
).then((token) {
Logger.d("Token from AuthManager recived");
_tempToken = token;
_doConnect(completer: completer, forceReconnect: forceReconnect);
}).catchError((e) {
completer.completeError(e);
});
} else {
_doConnect(completer: completer, forceReconnect: forceReconnect);
}
}
return completer.future;
}
void _doConnect({Completer completer, bool forceReconnect}) {
if (forceReconnect || !isConnected) {
_connect().timeout(connectTimeout, onTimeout: () {
_disconnect().then((_) {
completer?.completeError(HAError("Connection timeout"));
});
}).then((_) {
Logger.d("doConnect is finished 1");
completer?.complete();
}).catchError((e) {
completer?.completeError(e);
});
} else {
Logger.d("doConnect is finished 2");
completer?.complete();
}
}
Completer connecting;
Future _connect() {
if (connecting != null && !connecting.isCompleted) {
Logger.w("Previous connection attempt pending...");
return connecting.future;
} else {
connecting = Completer();
_disconnect().then((_) {
Logger.d("Socket connecting: $_webSocketAPIEndpoint...");
_socket = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
_socketSubscription = _socket.stream.listen(
(message) {
isConnected = true;
var data = json.decode(message);
if (data["type"] == "auth_required") {
Logger.d("[Received] <== ${data.toString()}");
_authenticate().then((_) {
Logger.d('Authentication complete');
connecting.complete();
}).catchError((e) {
if (!connecting.isCompleted) connecting.completeError(e);
});
} else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.complete();
_messageResolver.remove("auth");
if (_token != null) {
if (!connecting.isCompleted) connecting.complete();
}
} else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
_messageResolver.remove("auth");
logout().then((_) {
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
});
} else {
_handleMessage(data);
}
},
cancelOnError: true,
onDone: () => _handleSocketClose(connecting),
onError: (e) => _handleSocketError(e, connecting)
);
});
return connecting.future;
}
}
Future _disconnect() {
Completer completer = Completer();
if (!isConnected) {
completer.complete();
} else {
isConnected = false;
List<Future> fl = [];
Logger.d("Socket disconnecting...");
if (_socketSubscription != null) {
fl.add(_socketSubscription.cancel());
}
if (_socket != null && _socket.sink != null &&
_socket.closeCode == null) {
fl.add(_socket.sink.close().timeout(Duration(seconds: 3)));
}
Future.wait(fl).whenComplete(() => completer.complete());
}
return completer.future;
}
_handleMessage(data) {
if (data["type"] == "result") {
if (data["id"] != null && data["success"]) {
Logger.d("[Received] <== Request id ${data['id']} was successful");
_messageResolver["${data["id"]}"]?.complete(data["result"]);
} else if (data["id"] != null) {
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
_messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}");
}
_messageResolver.remove("${data["id"]}");
} else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
onStateChangeCallback(data["event"]["data"]);
} else if (data["event"] != null) {
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
} else {
Logger.e("Event is null: $data");
}
} else {
Logger.d("[Received unhandled] <== ${data.toString()}");
}
}
void _handleSocketClose(Completer connectionCompleter) {
Logger.d("Socket disconnected.");
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect().catchError((e) {
isConnected = false;
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
});
});
});
}
}
void _handleSocketError(e, Completer connectionCompleter) {
Logger.e("Socket stream Error: $e");
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect().catchError((e) {
isConnected = false;
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
});
});
});
}
}
Future _authenticate() {
Completer completer = Completer();
if (_token != null) {
Logger.d( "Long-lived token exist");
Logger.d( "[Sending] ==> auth request");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_token"},
auth: true
).then((_) {
completer.complete();
}).catchError((e) => completer.completeError(e));
} else if (_tempToken != null) {
Logger.d("We have temp token. Loging in...");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_tempToken"},
auth: true
).then((_) {
Logger.d("Requesting long-lived token...");
_getLongLivedToken().then((_) {
Logger.d("getLongLivedToken finished");
completer.complete();
}).catchError((e) {
Logger.e("Can't get long-lived token: $e");
throw e;
});
}).catchError((e) => completer.completeError(e));
} else {
completer.completeError(HAError("General login error"));
}
return completer.future;
}
Future logout() {
Completer completer = Completer();
_disconnect().whenComplete(() {
_token = null;
_tempToken = null;
final storage = new FlutterSecureStorage();
storage.delete(key: "hacl_llt").whenComplete((){
completer.complete();
});
});
return completer.future;
}
Future _getLongLivedToken() {
Completer completer = Completer();
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
Logger.d("Got long-lived token.");
_token = data;
_tempToken = null;
final storage = new FlutterSecureStorage();
storage.write(key: "hacl_llt", value: "$_token").then((_) {
completer.complete();
}).catchError((e) {
throw e;
});
}).catchError((e) {
logout();
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.loginAgain()]));
});
return completer.future;
}
Future sendSocketMessage({String type, Map additionalData, bool auth: false}) {
Completer _completer = Completer();
Map dataObject = {"type": "$type"};
String callbackName;
if (!auth) {
_incrementMessageId();
dataObject["id"] = _currentMessageId;
callbackName = "$_currentMessageId";
} else {
callbackName = "auth";
}
if (additionalData != null) {
dataObject.addAll(additionalData);
}
_messageResolver[callbackName] = _completer;
String rawMessage = json.encode(dataObject);
if (!isConnected) {
_connect().timeout(connectTimeout, onTimeout: (){
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
}).then((_) {
Logger.d("[Sending] ==> $rawMessage");
_socket.sink.add(rawMessage);
}).catchError((e) {
_completer.completeError(e);
});
} else {
Logger.d("[Sending] ==> $rawMessage");
_socket.sink.add(rawMessage);
}
return _completer.future;
}
void _incrementMessageId() {
_currentMessageId += 1;
}
Future callService({String domain, String service, String entityId, Map additionalServiceData}) {
Map serviceData = {};
if (entityId != null) {
serviceData["entity_id"] = entityId;
}
if (additionalServiceData != null && additionalServiceData.isNotEmpty) {
serviceData.addAll(additionalServiceData);
}
if (serviceData.isNotEmpty)
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
else
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
}
Future<List> getHistory(String entityId) async {
DateTime now = DateTime.now();
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String url = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
Logger.d("[Sending] ==> $url");
http.Response historyResponse;
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_token",
"Content-Type": "application/json"
});
var history = json.decode(historyResponse.body);
if (history is List) {
Logger.d( "[Received] <== ${history.first.length} history recors");
return history;
} else {
return [];
}
}
Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async {
Completer completer = Completer();
String url = "$httpWebHost$endPoint";
Logger.d("[Sending] ==> $url");
Map<String, String> headers = {};
if (contentType != null) {
headers["Content-Type"] = contentType;
}
if (includeAuthHeader) {
headers["authorization"] = "Bearer $_token";
}
http.post(
url,
headers: headers,
body: data
).then((response) {
Logger.d("[Received] <== ${response.statusCode}, ${response.body}");
if (response.statusCode >= 200 && response.statusCode < 300 ) {
completer.complete(response.body);
} else {
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
}
}).catchError((e) {
completer.completeError(e);
});
return completer.future;
}
}

29
lib/device.class.dart Normal file
View File

@ -0,0 +1,29 @@
part of 'main.dart';
class Device {
static final Device _instance = Device._internal();
factory Device() {
return _instance;
}
String unicDeviceId;
String manufacturer;
String model;
String osName;
String osVersion;
Device._internal();
loadDeviceInfo() {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
deviceInfo.androidInfo.then((androidInfo) {
unicDeviceId = "${androidInfo.model.toLowerCase().replaceAll(' ', '_')}_${androidInfo.androidId}";
manufacturer = "${androidInfo.manufacturer}";
model = "${androidInfo.model}";
osName = "Android";
osVersion = "${androidInfo.version.release}";
});
}
}

View File

@ -46,10 +46,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
// the App.build method, and use it to set our appbar title.
title: new Text(_title),
),
body: HomeAssistantModel(
homeAssistant: widget.homeAssistant,
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
),
body: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context),
);
}

View File

@ -1,7 +1,8 @@
part of '../main.dart';
class AlarmControlPanelEntity extends Entity {
AlarmControlPanelEntity(Map rawData) : super(rawData);
AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost);
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {

View File

@ -1,7 +1,8 @@
part of '../main.dart';
class AutomationEntity extends Entity {
AutomationEntity(Map rawData) : super(rawData);
AutomationEntity(Map rawData, String webHost) : super(rawData, webHost);
@override
Widget _buildStatePart(BuildContext context) {

View File

@ -1,7 +1,8 @@
part of '../main.dart';
class ButtonEntity extends Entity {
ButtonEntity(Map rawData) : super(rawData);
ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);
@override
Widget _buildStatePart(BuildContext context) {

View File

@ -4,7 +4,7 @@ class CameraEntity extends Entity {
static const SUPPORT_ON_OFF = 1;
CameraEntity(Map rawData) : super(rawData);
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportOnOff => ((supportedFeatures &
CameraEntity.SUPPORT_ON_OFF) ==

View File

@ -10,69 +10,57 @@ class ClimateEntity extends Entity {
);
static const SUPPORT_TARGET_TEMPERATURE = 1;
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
static const SUPPORT_TARGET_HUMIDITY = 8;
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
static const SUPPORT_FAN_MODE = 64;
static const SUPPORT_OPERATION_MODE = 128;
static const SUPPORT_HOLD_MODE = 256;
static const SUPPORT_SWING_MODE = 512;
static const SUPPORT_AWAY_MODE = 1024;
static const SUPPORT_AUX_HEAT = 2048;
static const SUPPORT_ON_OFF = 4096;
static const SUPPORT_TARGET_TEMPERATURE_RANGE = 2;
static const SUPPORT_TARGET_HUMIDITY = 4;
static const SUPPORT_FAN_MODE = 8;
static const SUPPORT_PRESET_MODE = 16;
static const SUPPORT_SWING_MODE = 32;
static const SUPPORT_AUX_HEAT = 64;
//static const SUPPORT_OPERATION_MODE = 16;
//static const SUPPORT_HOLD_MODE = 256;
//static const SUPPORT_AWAY_MODE = 1024;
//static const SUPPORT_ON_OFF = 4096;
ClimateEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportTargetTemperature => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
bool get supportTargetTemperatureHigh => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
bool get supportTargetTemperatureLow => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
bool get supportTargetTemperatureRange => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE);
bool get supportTargetHumidity => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
bool get supportTargetHumidityHigh => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
bool get supportTargetHumidityLow => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
bool get supportFanMode =>
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
ClimateEntity.SUPPORT_FAN_MODE);
bool get supportOperationMode => ((supportedFeatures &
ClimateEntity.SUPPORT_OPERATION_MODE) ==
ClimateEntity.SUPPORT_OPERATION_MODE);
bool get supportHoldMode =>
((supportedFeatures & ClimateEntity.SUPPORT_HOLD_MODE) ==
ClimateEntity.SUPPORT_HOLD_MODE);
bool get supportSwingMode =>
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
ClimateEntity.SUPPORT_SWING_MODE);
bool get supportAwayMode =>
((supportedFeatures & ClimateEntity.SUPPORT_AWAY_MODE) ==
ClimateEntity.SUPPORT_AWAY_MODE);
bool get supportPresetMode =>
((supportedFeatures & ClimateEntity.SUPPORT_PRESET_MODE) ==
ClimateEntity.SUPPORT_PRESET_MODE);
bool get supportAuxHeat =>
((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) ==
ClimateEntity.SUPPORT_AUX_HEAT);
bool get supportOnOff =>
((supportedFeatures & ClimateEntity.SUPPORT_ON_OFF) ==
ClimateEntity.SUPPORT_ON_OFF);
List<String> get operationList => attributes["operation_list"] != null
? (attributes["operation_list"] as List).cast<String>()
List<String> get hvacModes => attributes["hvac_modes"] != null
? (attributes["hvac_modes"] as List).cast<String>()
: null;
List<String> get fanList => attributes["fan_list"] != null
? (attributes["fan_list"] as List).cast<String>()
List<String> get fanModes => attributes["fan_modes"] != null
? (attributes["fan_modes"] as List).cast<String>()
: null;
List<String> get swingList => attributes["swing_list"] != null
? (attributes["swing_list"] as List).cast<String>()
List<String> get presetModes => attributes["preset_modes"] != null
? (attributes["preset_modes"] as List).cast<String>()
: null;
List<String> get swingModes => attributes["swing_modes"] != null
? (attributes["swing_modes"] as List).cast<String>()
: null;
double get temperature => _getDoubleAttributeValue('temperature');
double get currentTemperature => _getDoubleAttributeValue('current_temperature');
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
double get targetLow => _getDoubleAttributeValue('target_temp_low');
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
@ -81,25 +69,22 @@ class ClimateEntity extends Entity {
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
double get minHumidity => _getDoubleAttributeValue('min_humidity');
double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5;
String get operationMode => attributes['operation_mode'];
String get hvacAction => attributes['hvac_action'];
String get fanMode => attributes['fan_mode'];
String get presetMode => attributes['preset_mode'];
String get swingMode => attributes['swing_mode'];
bool get awayMode => attributes['away_mode'] == "on";
bool get isOff => state == EntityState.off;
//bool get isOff => state == EntityState.off;
bool get auxHeat => attributes['aux_heat'] == "on";
ClimateEntity(Map rawData) : super(rawData);
@override
void update(Map rawData) {
super.update(rawData);
void update(Map rawData, String webHost) {
super.update(rawData, webHost);
if (supportTargetTemperature) {
historyConfig.numericAttributesToShow.add("temperature");
}
if (supportTargetTemperatureHigh) {
if (supportTargetTemperatureRange) {
historyConfig.numericAttributesToShow.add("target_temp_high");
}
if (supportTargetTemperatureLow) {
historyConfig.numericAttributesToShow.add("target_temp_low");
}
}

View File

@ -11,6 +11,8 @@ class CoverEntity extends Entity {
static const SUPPORT_STOP_TILT = 64;
static const SUPPORT_SET_TILT_POSITION = 128;
CoverEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportOpen => ((supportedFeatures &
CoverEntity.SUPPORT_OPEN) ==
CoverEntity.SUPPORT_OPEN);
@ -45,8 +47,6 @@ class CoverEntity extends Entity {
bool get canTiltBeOpened => currentTiltPosition < 100;
bool get canTiltBeClosed => currentTiltPosition > 0;
CoverEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return CoverStateWidget();

View File

@ -1,6 +1,8 @@
part of '../main.dart';
class DateTimeEntity extends Entity {
DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get hasDate => attributes["has_date"] ?? false;
bool get hasTime => attributes["has_time"] ?? false;
int get year => attributes["year"] ?? 1970;
@ -12,8 +14,6 @@ class DateTimeEntity extends Entity {
String get formattedState => _getFormattedState();
DateTime get dateTimeState => _getDateTimeState();
DateTimeEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return DateTimeStateWidget();

View File

@ -27,7 +27,7 @@ class Entity {
"cold.on": "Cold",
"cold.off": "Normal",
"connectivity.on": "Connected",
"connectivity.off": "Diconnected",
"connectivity.off": "Disconnected",
"door.on": "Open",
"door.off": "Closed",
"garage_door.on": "Open",
@ -73,6 +73,7 @@ class Entity {
Map attributes;
String domain;
String entityId;
String entityPicture;
String state;
String displayState;
DateTime _lastUpdated;
@ -94,7 +95,6 @@ class Entity {
bool get isBadge => Entity.badgeDomains.contains(domain);
String get icon => attributes["icon"] ?? "";
bool get isOn => state == EntityState.on;
String get entityPicture => _getEntityPictureUrl();
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
List get childEntityIds => attributes["entity_id"] ?? [];
String get lastUpdated => _getLastUpdatedFormatted();
@ -102,21 +102,21 @@ class Entity {
double get doubleState => double.tryParse(state) ?? 0.0;
int get supportedFeatures => attributes["supported_features"] ?? 0;
String _getEntityPictureUrl() {
String _getEntityPictureUrl(String webHost) {
String result = attributes["entity_picture"];
if (result == null) return result;
if (!result.startsWith("http")) {
if (result.startsWith("/")) {
result = "$homeAssistantWebHost$result";
result = "$webHost$result";
} else {
result = "$homeAssistantWebHost/$result";
result = "$webHost/$result";
}
}
return result;
}
Entity(Map rawData) {
update(rawData);
Entity(Map rawData, String webHost) {
update(rawData, webHost);
}
Entity.missed(String entityId) {
@ -148,14 +148,15 @@ class Entity {
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
}
void update(Map rawData) {
void update(Map rawData, String webHost) {
attributes = rawData["attributes"] ?? {};
domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"];
deviceClass = attributes["device_class"];
state = rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
entityPicture = _getEntityPictureUrl(webHost);
}
double _getDoubleAttributeValue(String attributeName) {
@ -215,7 +216,7 @@ class Entity {
entityWrapper: EntityWrapper(entity: this),
child: EntityPageContainer(children: <Widget>[
Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding),
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
),
LastUpdatedWidget(),

View File

@ -6,7 +6,7 @@ class FanEntity extends Entity {
static const SUPPORT_OSCILLATE = 2;
static const SUPPORT_DIRECTION = 4;
FanEntity(Map rawData) : super(rawData);
FanEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportSetSpeed => ((supportedFeatures &
FanEntity.SUPPORT_SET_SPEED) ==

View File

@ -1,12 +1,13 @@
part of '../main.dart';
class GroupEntity extends Entity {
GroupEntity(Map rawData) : super(rawData);
final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"];
String mutualDomain;
bool switchable = false;
GroupEntity(Map rawData, String webHost) : super(rawData, webHost);
@override
Widget _buildStatePart(BuildContext context) {
if (switchable) {
@ -19,8 +20,8 @@ class GroupEntity extends Entity {
}
@override
void update(Map rawData) {
super.update(rawData);
void update(Map rawData, String webHost) {
super.update(rawData, webHost);
if (_isOneDomain()) {
mutualDomain = attributes['entity_id'][0].split(".")[0];
switchable = _domainsForSwitchableGroup.contains(mutualDomain);

View File

@ -42,7 +42,7 @@ class LightEntity extends Entity {
bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0));
List<String> get effectList => getStringListAttributeValue("effect_list");
LightEntity(Map rawData) : super(rawData);
LightEntity(Map rawData, String webHost) : super(rawData, webHost);
HSVColor _getColor() {
List hs = attributes["hs_color"];

View File

@ -1,7 +1,7 @@
part of '../main.dart';
class LockEntity extends Entity {
LockEntity(Map rawData) : super(rawData);
LockEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get isLocked => state == "locked";

View File

@ -20,7 +20,7 @@ class MediaPlayerEntity extends Entity {
static const SUPPORT_SHUFFLE_SET = 32768;
static const SUPPORT_SELECT_SOUND_MODE = 65536;
MediaPlayerEntity(Map rawData) : super(rawData);
MediaPlayerEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportPause => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_PAUSE) ==

View File

@ -1,7 +1,7 @@
part of '../main.dart';
class SunEntity extends Entity {
SunEntity(Map rawData) : super(rawData);
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
}
class SensorEntity extends Entity {
@ -12,6 +12,6 @@ class SensorEntity extends Entity {
numericState: true
);
SensorEntity(Map rawData) : super(rawData);
SensorEntity(Map rawData, String webHost) : super(rawData, webHost);
}

View File

@ -5,7 +5,7 @@ class SelectEntity extends Entity {
? (attributes["options"] as List).cast<String>()
: [];
SelectEntity(Map rawData) : super(rawData);
SelectEntity(Map rawData, String webHost) : super(rawData, webHost);
@override
Widget _buildStatePart(BuildContext context) {

View File

@ -1,7 +1,7 @@
part of '../main.dart';
class SliderEntity extends Entity {
SliderEntity(Map rawData) : super(rawData);
SliderEntity(Map rawData, String webHost) : super(rawData, webHost);
double get minValue => _getDoubleAttributeValue("min") ?? 0.0;
double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0;

View File

@ -1,7 +1,7 @@
part of '../main.dart';
class SwitchEntity extends Entity {
SwitchEntity(Map rawData) : super(rawData);
SwitchEntity(Map rawData, String webHost) : super(rawData, webHost);
@override
Widget _buildStatePart(BuildContext context) {

View File

@ -1,7 +1,7 @@
part of '../main.dart';
class TextEntity extends Entity {
TextEntity(Map rawData) : super(rawData);
TextEntity(Map rawData, String webHost) : super(rawData, webHost);
int get valueMinLength => attributes["min"] ?? -1;
int get valueMaxLength => attributes["max"] ?? -1;

View File

@ -1,30 +1,39 @@
part of '../main.dart';
class TimerEntity extends Entity {
TimerEntity(Map rawData) : super(rawData);
TimerEntity(Map rawData, String webHost) : super(rawData, webHost);
Duration duration;
@override
void update(Map rawData) {
super.update(rawData);
void update(Map rawData, String webHost) {
super.update(rawData, webHost);
String durationSource = "${attributes["duration"]}";
List<String> durationList = durationSource.split(":");
if (durationList.length == 1) {
duration = Duration(seconds: int.tryParse(durationList[0] ?? 0));
} else if (durationList.length == 2) {
duration = Duration(
hours: int.tryParse(durationList[0]) ?? 0,
minutes: int.tryParse(durationList[1]) ?? 0
);
} else if (durationList.length == 3) {
duration = Duration(
hours: int.tryParse(durationList[0]) ?? 0,
minutes: int.tryParse(durationList[1]) ?? 0,
seconds: int.tryParse(durationList[2]) ?? 0
);
if (durationSource != null && durationSource.isNotEmpty) {
try {
List<String> durationList = durationSource.split(":");
if (durationList.length == 1) {
duration = Duration(seconds: int.tryParse(durationList[0] ?? 0));
} else if (durationList.length == 2) {
duration = Duration(
hours: int.tryParse(durationList[0]) ?? 0,
minutes: int.tryParse(durationList[1]) ?? 0
);
} else if (durationList.length == 3) {
duration = Duration(
hours: int.tryParse(durationList[0]) ?? 0,
minutes: int.tryParse(durationList[1]) ?? 0,
seconds: int.tryParse(durationList[2]) ?? 0
);
} else {
Logger.e("Strange $entityId duration format: $durationSource");
duration = Duration(seconds: 0);
}
} catch (e) {
Logger.e("Error parsing duration for $entityId: ${e.toString()}");
duration = Duration(seconds: 0);
}
} else {
Logger.e("Cann't parse $entityId duration: $durationSource");
duration = Duration(seconds: 0);
}
}

View File

@ -2,13 +2,15 @@ part of 'main.dart';
class EntityCollection {
final homeAssistantWebHost;
Map<String, Entity> _allEntities;
//Map<String, Entity> views;
bool get isEmpty => _allEntities.isEmpty;
List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList();
EntityCollection() {
EntityCollection(this.homeAssistantWebHost) {
_allEntities = {};
//views = {};
}
@ -33,70 +35,74 @@ class EntityCollection {
});
}
void clear() {
_allEntities.clear();
}
Entity _createEntityInstance(rawEntityData) {
switch (rawEntityData["entity_id"].split(".")[0]) {
case 'sun': {
return SunEntity(rawEntityData);
return SunEntity(rawEntityData, homeAssistantWebHost);
}
case "media_player": {
return MediaPlayerEntity(rawEntityData);
return MediaPlayerEntity(rawEntityData, homeAssistantWebHost);
}
case 'sensor': {
return SensorEntity(rawEntityData);
return SensorEntity(rawEntityData, homeAssistantWebHost);
}
case 'lock': {
return LockEntity(rawEntityData);
return LockEntity(rawEntityData, homeAssistantWebHost);
}
case "automation": {
return AutomationEntity(rawEntityData);
return AutomationEntity(rawEntityData, homeAssistantWebHost);
}
case "input_boolean":
case "switch": {
return SwitchEntity(rawEntityData);
return SwitchEntity(rawEntityData, homeAssistantWebHost);
}
case "light": {
return LightEntity(rawEntityData);
return LightEntity(rawEntityData, homeAssistantWebHost);
}
case "group": {
return GroupEntity(rawEntityData);
return GroupEntity(rawEntityData, homeAssistantWebHost);
}
case "script":
case "scene": {
return ButtonEntity(rawEntityData);
return ButtonEntity(rawEntityData, homeAssistantWebHost);
}
case "input_datetime": {
return DateTimeEntity(rawEntityData);
return DateTimeEntity(rawEntityData, homeAssistantWebHost);
}
case "input_select": {
return SelectEntity(rawEntityData);
return SelectEntity(rawEntityData, homeAssistantWebHost);
}
case "input_number": {
return SliderEntity(rawEntityData);
return SliderEntity(rawEntityData, homeAssistantWebHost);
}
case "input_text": {
return TextEntity(rawEntityData);
return TextEntity(rawEntityData, homeAssistantWebHost);
}
case "climate": {
return ClimateEntity(rawEntityData);
return ClimateEntity(rawEntityData, homeAssistantWebHost);
}
case "cover": {
return CoverEntity(rawEntityData);
return CoverEntity(rawEntityData, homeAssistantWebHost);
}
case "fan": {
return FanEntity(rawEntityData);
return FanEntity(rawEntityData, homeAssistantWebHost);
}
case "camera": {
return CameraEntity(rawEntityData);
return CameraEntity(rawEntityData, homeAssistantWebHost);
}
case "alarm_control_panel": {
return AlarmControlPanelEntity(rawEntityData);
return AlarmControlPanelEntity(rawEntityData, homeAssistantWebHost);
}
case "timer": {
return TimerEntity(rawEntityData);
return TimerEntity(rawEntityData, homeAssistantWebHost);
}
default: {
return Entity(rawEntityData);
return Entity(rawEntityData, homeAssistantWebHost);
}
}
}
@ -121,7 +127,7 @@ class EntityCollection {
}
void updateFromRaw(Map rawEntityData) {
get("${rawEntityData["entity_id"]}")?.update(rawEntityData);
get("${rawEntityData["entity_id"]}")?.update(rawEntityData, homeAssistantWebHost);
}
Entity get(String entityId) {

View File

@ -35,25 +35,36 @@ class BadgeWidget extends StatelessWidget {
break;
}
case "device_tracker":
case "person":
{
badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
);
onBadgeTextValue = entityModel.entityWrapper.entity.state;
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
break;
}
default:
{
double stateFontSize;
if (entityModel.entityWrapper.entity.displayState.length <= 3) {
stateFontSize = 18.0;
} else if (entityModel.entityWrapper.entity.displayState.length <= 4) {
stateFontSize = 15.0;
} else if (entityModel.entityWrapper.entity.displayState.length <= 6) {
stateFontSize = 10.0;
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
stateFontSize = 8.0;
}
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
badgeIcon = Center(
child: Text(
"${entityModel.entityWrapper.entity.state}",
"${entityModel.entityWrapper.entity.displayState}",
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 17.0),
style: TextStyle(fontSize: stateFontSize),
),
);
break;

View File

@ -16,111 +16,26 @@ class _CameraStreamViewState extends State<CameraStreamView> {
}
CameraEntity _entity;
http.Client client;
http.StreamedResponse response;
List<int> binaryImage = [];
bool timeToStop = false;
Completer streamCompleter;
bool started = false;
bool useSVG = false;
String streamUrl = "";
void _connect() async {
started = true;
timeToStop = false;
String streamUrl = '$homeAssistantWebHost/api/camera_proxy_stream/${_entity.entityId}?token=${_entity.attributes['access_token']}';
client = new http.Client(); // create a client to make api calls
http.Request request = new http.Request("GET", Uri.parse(streamUrl)); // create get request
Logger.d("[Sending] ==> $streamUrl");
response = await client.send(request);
Logger.d("[Received] <== ${response.headers}");
String frameBoundary = response.headers['content-type'].split('boundary=')[1];
final int frameBoundarySize = frameBoundary.length;
List<int> primaryBuffer=[];
int imageSizeStart = 59;
int imageSizeEnd = 0;
int imageStart = 0;
int imageSize = 0;
String strBuffer = "";
String contentType = "";
streamCompleter = Completer();
response.stream.transform(
StreamTransformer.fromHandlers(
handleData: (data, sink) {
primaryBuffer.addAll(data);
imageStart = 0;
imageSizeEnd = 0;
if (primaryBuffer.length >= imageSizeStart + 10) {
contentType = utf8.decode(
primaryBuffer.sublist(frameBoundarySize+16, imageSizeStart + 10), allowMalformed: true).split("\r\n")[0];
useSVG = contentType == "image/svg+xml";
imageSizeStart = frameBoundarySize + 16 + contentType.length + 18;
for (int i = imageSizeStart; i < primaryBuffer.length - 4; i++) {
strBuffer = utf8.decode(
primaryBuffer.sublist(i, i + 4), allowMalformed: true);
if (strBuffer == "\r\n\r\n") {
imageSizeEnd = i;
imageStart = i + 4;
break;
}
}
if (imageSizeEnd > 0) {
imageSize = int.tryParse(utf8.decode(
primaryBuffer.sublist(imageSizeStart, imageSizeEnd),
allowMalformed: true));
//Logger.d("content-length: $imageSize");
if (imageSize != null &&
primaryBuffer.length >= imageStart + imageSize + 2) {
sink.add(
primaryBuffer.sublist(
imageStart, imageStart + imageSize));
primaryBuffer.removeRange(0, imageStart + imageSize + 2);
}
}
}
if (timeToStop) {
sink?.close();
streamCompleter.complete();
}
},
handleError: (error, stack, sink) {
Logger.e("Error parsing MJPEG stream: $error");
},
handleDone: (sink) {
Logger.d("Camera stream finished. Reconnecting...");
sink?.close();
streamCompleter?.complete();
_reconnect();
},
launchStream() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebviewScaffold(
url: "$streamUrl",
withZoom: true,
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.close),
onPressed: () => Navigator.pop(context)
),
title: new Text("${_entity.displayName}"),
),
),
)
).listen((d) {
if (!timeToStop) {
setState(() {
binaryImage = d;
});
}
});
}
void _reconnect() {
disconnect().then((_){
_connect();
});
}
Future disconnect() {
Completer disconF = Completer();
timeToStop = true;
if (streamCompleter != null && !streamCompleter.isCompleted) {
streamCompleter.future.then((_) {
client?.close();
disconF.complete();
});
} else {
client?.close();
disconF.complete();
}
return disconF.future;
);
}
@override
@ -130,46 +45,26 @@ class _CameraStreamViewState extends State<CameraStreamView> {
.of(context)
.entityWrapper
.entity;
_connect();
started = true;
}
if (binaryImage.isEmpty) {
return Column(
children: <Widget>[
Container(
padding: const EdgeInsets.all(20.0),
child: const CircularProgressIndicator()
)
],
);
} else {
if (useSVG) {
return Column(
children: <Widget>[
SvgPicture.memory(
Uint8List.fromList(binaryImage),
placeholderBuilder: (BuildContext context) =>
new Container(
padding: const EdgeInsets.all(20.0),
child: const CircularProgressIndicator()
),
streamUrl = '${Connection().httpWebHost}/api/camera_proxy_stream/${_entity
.entityId}?token=${_entity.attributes['access_token']}';
return Column(
children: <Widget>[
Container(
padding: const EdgeInsets.all(20.0),
child: IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
iconSize: 50.0,
onPressed: () => launchStream(),
)
],
);
} else {
return Column(
children: <Widget>[
Image.memory(
Uint8List.fromList(binaryImage), gaplessPlayback: true),
],
);
}
}
)
],
);
}
@override
void dispose() {
disconnect();
super.dispose();
}
}

View File

@ -19,22 +19,22 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
double _tmpTargetLow = 0.0;
double _tmpTargetHigh = 0.0;
double _tmpTargetHumidity = 0.0;
String _tmpOperationMode;
String _tmpHVACMode;
String _tmpFanMode;
String _tmpSwingMode;
bool _tmpAwayMode = false;
bool _tmpIsOff = false;
String _tmpPresetMode;
//bool _tmpIsOff = false;
bool _tmpAuxHeat = false;
void _resetVars(ClimateEntity entity) {
_tmpTemperature = entity.temperature;
_tmpTargetHigh = entity.targetHigh;
_tmpTargetLow = entity.targetLow;
_tmpOperationMode = entity.operationMode;
_tmpHVACMode = entity.state;
_tmpFanMode = entity.fanMode;
_tmpSwingMode = entity.swingMode;
_tmpAwayMode = entity.awayMode;
_tmpIsOff = entity.isOff;
_tmpPresetMode = entity.presetMode;
//_tmpIsOff = entity.isOff;
_tmpAuxHeat = entity.auxHeat;
_tmpTargetHumidity = entity.targetHumidity;
@ -116,11 +116,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
});
}
void _setOperationMode(ClimateEntity entity, value) {
void _setHVACMode(ClimateEntity entity, value) {
setState(() {
_tmpOperationMode = value;
_tmpHVACMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_operation_mode", entity.entityId,{"operation_mode": "$_tmpOperationMode"}));
eventBus.fire(new ServiceCallEvent(entity.domain, "set_hvac_mode", entity.entityId,{"hvac_mode": "$_tmpHVACMode"}));
_resetStateTimer(entity);
});
}
@ -143,23 +143,23 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
});
}
void _setAwayMode(ClimateEntity entity, value) {
void _setPresetMode(ClimateEntity entity, value) {
setState(() {
_tmpAwayMode = value;
_tmpPresetMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_away_mode", entity.entityId,{"away_mode": "${_tmpAwayMode ? 'on' : 'off'}"}));
eventBus.fire(new ServiceCallEvent(entity.domain, "set_preset_mode", entity.entityId,{"preset_mode": "$_tmpPresetMode"}));
_resetStateTimer(entity);
});
}
void _setOnOf(ClimateEntity entity, value) {
/*void _setOnOf(ClimateEntity entity, value) {
setState(() {
_tmpIsOff = !value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
_resetStateTimer(entity);
});
}
}*/
void _setAuxHeat(ClimateEntity entity, value) {
setState(() {
@ -196,33 +196,34 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildOnOffControl(entity),
//_buildOnOffControl(entity),
_buildTemperatureControls(entity),
_buildTargetTemperatureControls(entity),
_buildHumidityControls(entity),
_buildOperationControl(entity),
_buildFanControl(entity),
_buildSwingControl(entity),
_buildAwayModeControl(entity),
_buildPresetModeControl(entity),
_buildAuxHeatControl(entity)
],
),
);
}
Widget _buildAwayModeControl(ClimateEntity entity) {
if (entity.supportAwayMode) {
return ModeSwitchWidget(
caption: "Away mode",
onChange: (value) => _setAwayMode(entity, value),
value: _tmpAwayMode,
Widget _buildPresetModeControl(ClimateEntity entity) {
if (entity.supportPresetMode) {
return ModeSelectorWidget(
options: entity.presetModes,
onChange: (mode) => _setPresetMode(entity, mode),
caption: "Preset",
value: _tmpPresetMode,
);
} else {
return Container(height: 0.0, width: 0.0,);
}
}
Widget _buildOnOffControl(ClimateEntity entity) {
/*Widget _buildOnOffControl(ClimateEntity entity) {
if (entity.supportOnOff) {
return ModeSwitchWidget(
onChange: (value) => _setOnOf(entity, value),
@ -232,7 +233,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
} else {
return Container(height: 0.0, width: 0.0,);
}
}
}*/
Widget _buildAuxHeatControl(ClimateEntity entity) {
if (entity.supportAuxHeat ) {
@ -247,12 +248,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
Widget _buildOperationControl(ClimateEntity entity) {
if (entity.supportOperationMode) {
if (entity.hvacModes != null) {
return ModeSelectorWidget(
onChange: (mode) => _setOperationMode(entity, mode),
options: entity.operationList,
onChange: (mode) => _setHVACMode(entity, mode),
options: entity.hvacModes,
caption: "Operation",
value: _tmpOperationMode,
value: _tmpHVACMode,
);
} else {
return Container(height: 0.0, width: 0.0);
@ -262,7 +263,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
Widget _buildFanControl(ClimateEntity entity) {
if (entity.supportFanMode) {
return ModeSelectorWidget(
options: entity.fanList,
options: entity.fanModes,
onChange: (mode) => _setFanMode(entity, mode),
caption: "Fan mode",
value: _tmpFanMode,
@ -276,7 +277,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
if (entity.supportSwingMode) {
return ModeSelectorWidget(
onChange: (mode) => _setSwingMode(entity, mode),
options: entity.swingList,
options: entity.swingModes,
value: _tmpSwingMode,
caption: "Swing mode"
);
@ -308,7 +309,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
List<Widget> controls = [];
if ((entity.supportTargetTemperatureLow) && (entity.targetLow != null)) {
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
controls.addAll(<Widget>[
TemperatureControlWidget(
value: _tmpTargetLow,
@ -321,7 +322,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
)
]);
}
if ((entity.supportTargetTemperatureHigh) && (entity.targetHigh != null)) {
if ((entity.supportTargetTemperatureRange) && (entity.targetHigh != null)) {
controls.add(
TemperatureControlWidget(
value: _tmpTargetHigh,

View File

@ -171,7 +171,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
Widget _buildColorControl(LightEntity entity) {
if (entity.supportColor) {
HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor;
HSVColor savedColor = HomeAssistant().savedColor;
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
@ -187,10 +187,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
child: Text('Copy color'),
onPressed: _tmpColor == null ? null : () {
setState(() {
HomeAssistantModel
.of(context)
.homeAssistant
.savedColor = _tmpColor;
HomeAssistant().savedColor = _tmpColor;
});
},
),

View File

@ -73,7 +73,7 @@ class MediaPlayerWidget extends StatelessWidget {
Widget _buildImage(MediaPlayerEntity entity) {
String state = entity.state;
if (homeAssistantWebHost != null && entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
return Container(
color: Colors.black,
child: Row(

View File

@ -15,14 +15,18 @@ class DefaultEntityContainer extends StatelessWidget {
return MissedEntityWidget();
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
return Divider();
return Divider(
color: Colors.black45,
);
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Divider(),
Divider(
color: Colors.black45,
),
Text(
"${entityModel.entityWrapper.entity.displayName}",
style: TextStyle(color: Colors.blue),

View File

@ -40,14 +40,14 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
_needToUpdateHistory = true;
}
void _loadHistory(HomeAssistant ha, String entityId) {
void _loadHistory(String entityId) {
DateTime now = DateTime.now();
if (_historyLastUpdated != null) {
Logger.d("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
}
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
_historyLastUpdated = now;
ha.getHistory(entityId).then((history){
Connection().getHistory(entityId).then((history){
if (!_disposed) {
setState(() {
_history = history.isNotEmpty ? history[0] : [];
@ -68,13 +68,12 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
@override
Widget build(BuildContext context) {
final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context);
final EntityModel entityModel = EntityModel.of(context);
final Entity entity = entityModel.entityWrapper.entity;
if (!_needToUpdateHistory) {
_needToUpdateHistory = true;
} else {
_loadHistory(homeAssistantModel.homeAssistant, entity.entityId);
_loadHistory(entity.entityId);
}
return _buildChart();
}

View File

@ -15,26 +15,6 @@ class EntityModel extends InheritedWidget {
return context.inheritFromWidgetOfExactType(EntityModel);
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
}
}
class HomeAssistantModel extends InheritedWidget {
const HomeAssistantModel({
Key key,
@required this.homeAssistant,
@required Widget child,
}) : super(key: key, child: child);
final HomeAssistant homeAssistant;
static HomeAssistantModel of(BuildContext context) {
return context.inheritFromWidgetOfExactType(HomeAssistantModel);
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;

View File

@ -8,13 +8,19 @@ class ClimateStateWidget extends StatelessWidget {
String targetTemp = "-";
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
targetTemp = "${entity.temperature}";
} else if ((entity.supportTargetTemperatureLow) &&
(entity.targetLow != null)) {
targetTemp = "${entity.targetLow}";
if ((entity.supportTargetTemperatureHigh) &&
(entity.targetHigh != null)) {
targetTemp += " - ${entity.targetHigh}";
}
} else if ((entity.supportTargetTemperatureRange) &&
(entity.targetLow != null) &&
(entity.targetHigh != null)) {
targetTemp = "${entity.targetLow} - ${entity.targetHigh}";
}
String displayState = '';
if (entity.hvacAction != null) {
displayState = "${entity.hvacAction} (${entity.displayState})";
} else {
displayState = "${entity.displayState}";
}
if (entity.presetMode != null) {
displayState += " - ${entity.presetMode}";
}
return Padding(
padding: EdgeInsets.fromLTRB(
@ -25,7 +31,7 @@ class ClimateStateWidget extends StatelessWidget {
children: <Widget>[
Row(
children: <Widget>[
Text("${entity.state}",
Text("$displayState",
textAlign: TextAlign.right,
style: new TextStyle(
fontWeight: FontWeight.bold,
@ -38,8 +44,8 @@ class ClimateStateWidget extends StatelessWidget {
))
],
),
entity.attributes["current_temperature"] != null ?
Text("Currently: ${entity.attributes["current_temperature"]}",
entity.currentTemperature != null ?
Text("Currently: ${entity.currentTemperature}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,

View File

@ -1,40 +1,29 @@
part of 'main.dart';
class HomeAssistant {
String _webSocketAPIEndpoint;
String _password;
bool _useLovelace = false;
IOWebSocketChannel _hassioChannel;
SendMessageQueue _messageQueue;
static final HomeAssistant _instance = HomeAssistant._internal();
factory HomeAssistant() {
return _instance;
}
int _currentMessageId = 0;
int _subscriptionMessageId = 0;
Map<int, Completer> _messageResolver = {};
EntityCollection entities;
HomeAssistantUI ui;
Map _instanceConfig = {};
String _userName;
HSVColor savedColor;
String fcmToken;
Map _rawLovelaceData;
List<Panel> panels = [];
Completer _fetchCompleter;
Completer _connectionCompleter;
Timer _connectionTimer;
Timer _fetchTimer;
bool autoReconnect = false;
StreamSubscription _socketSubscription;
int messageExpirationTime = 30; //seconds
Duration fetchTimeout = Duration(seconds: 30);
Duration connectTimeout = Duration(seconds: 15);
String get locationName {
if (_useLovelace) {
if (Connection().useLovelace) {
return ui?.title ?? "";
} else {
return _instanceConfig["location_name"] ?? "";
@ -42,323 +31,234 @@ class HomeAssistant {
}
String get userName => _userName ?? locationName;
String get userAvatarText => userName.length > 0 ? userName[0] : "";
//int get viewsCount => entities.views.length ?? 0;
bool get isNoEntities => entities == null || entities.isEmpty;
bool get isNoViews => ui == null || ui.isEmpty;
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
HomeAssistant() {
entities = EntityCollection();
_messageQueue = SendMessageQueue(messageExpirationTime);
HomeAssistant._internal() {
Connection().onStateChangeCallback = _handleEntityStateChange;
Device().loadDeviceInfo();
}
void updateSettings(String url, String password, bool useLovelace) {
_webSocketAPIEndpoint = url;
_password = password;
_useLovelace = useLovelace;
Logger.d( "Use lovelace is $_useLovelace");
}
Completer _fetchCompleter;
Future fetch() {
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
Logger.w("Previous fetch is not complited");
} else {
_fetchCompleter = new Completer();
_fetchTimer = Timer(fetchTimeout, () {
Logger.e( "Data fetching timeout");
disconnect().then((_) {
_completeFetching({
"errorCode": 9,
"errorMessage": "Couldn't get data from server"
});
});
});
_connection().then((r) {
_getData();
}).catchError((e) {
_completeFetching(e);
});
Future fetchData() {
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
Logger.w("Previous data fetch is not completed yet");
return _fetchCompleter.future;
}
return _fetchCompleter.future;
}
disconnect() async {
if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) {
await _hassioChannel.sink.close().timeout(Duration(seconds: 3),
onTimeout: () => Logger.d( "Socket sink closed")
);
await _socketSubscription.cancel();
_hassioChannel = null;
}
}
Future _connection() {
if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
Logger.d("Previous connection is not complited");
} else {
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
_connectionCompleter = new Completer();
autoReconnect = false;
disconnect().then((_){
Logger.d( "Socket connecting...");
_connectionTimer = Timer(connectTimeout, () {
Logger.e( "Socket connection timeout");
_handleSocketError(null);
});
if (_socketSubscription != null) {
_socketSubscription.cancel();
}
_hassioChannel = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 30));
_socketSubscription = _hassioChannel.stream.listen(
(message) => _handleMessage(message),
cancelOnError: true,
onDone: () => _handleSocketClose(),
onError: (e) => _handleSocketError(e)
);
});
} else {
_completeConnecting(null);
}
}
return _connectionCompleter.future;
}
void _handleSocketClose() {
Logger.d("Socket disconnected. Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
}
}
void _handleSocketError(e) {
Logger.e("Socket stream Error: $e");
Logger.d("Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
} else {
disconnect().then((_) {
_completeConnecting({
"errorCode": 1,
"errorMessage": "Couldn't connect to Home Assistant. Check network connection or connection settings."
});
});
}
}
void _reconnect() {
disconnect().then((_) {
_connection().catchError((e){
_completeConnecting(e);
});
});
}
_getData() async {
if (entities == null) entities = EntityCollection(Connection().httpWebHost);
_fetchCompleter = Completer();
List<Future> futures = [];
futures.add(_getStates());
if (_useLovelace) {
if (Connection().useLovelace) {
futures.add(_getLovelace());
}
futures.add(_getConfig());
futures.add(_getServices());
futures.add(_getUserInfo());
futures.add(_getPanels());
try {
await Future.wait(futures);
_createUI();
_completeFetching(null);
} catch (error) {
_completeFetching(error);
}
}
void _completeFetching(error) {
_fetchTimer.cancel();
_completeConnecting(error);
if (!_fetchCompleter.isCompleted) {
if (error != null) {
_fetchCompleter.completeError(error);
} else {
autoReconnect = true;
Logger.d( "Fetch complete successful");
futures.add(Connection().sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "state_changed"},
));
Future.wait(futures).then((_) {
if (isMobileAppEnabled) {
_createUI();
_fetchCompleter.complete();
checkAppRegistration();
} else {
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app")]));
}
}
}).catchError((e) {
_fetchCompleter.completeError(e);
});
return _fetchCompleter.future;
}
void _completeConnecting(error) {
_connectionTimer.cancel();
if (!_connectionCompleter.isCompleted) {
if (error != null) {
_connectionCompleter.completeError(error);
} else {
_connectionCompleter.complete();
}
} else if (error != null) {
if (error is Error) {
eventBus.fire(ShowErrorEvent(error.toString(), 12));
} else {
eventBus.fire(ShowErrorEvent(error["errorMessage"], error["errorCode"]));
}
}
Future logout() async {
Logger.d("Logging out...");
await Connection().logout().then((_) {
ui?.clear();
entities?.clear();
panels?.clear();
});
}
_handleMessage(String message) {
var data = json.decode(message);
if (data["type"] == "auth_required") {
_sendAuthMessage('{"type": "auth","access_token": "$_password"}');
} else if (data["type"] == "auth_ok") {
_completeConnecting(null);
_sendSubscribe();
} else if (data["type"] == "auth_invalid") {
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") {
Logger.d("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}");
_messageResolver[data["id"]]?.complete(data);
_messageResolver.remove(data["id"]);
} else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
_handleEntityStateChange(data["event"]["data"]);
} else if (data["event"] != null) {
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
} else {
Logger.e("Event is null: $message");
Map _getAppRegistrationData() {
return {
"app_version": "$appVersion",
"device_name": "$userName's ${Device().model}",
"manufacturer": Device().manufacturer,
"model": Device().model,
"os_name": Device().osName,
"os_version": Device().osVersion,
"app_data": {
"push_token": "$fcmToken",
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
}
};
}
Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
Completer completer = Completer();
if (Connection().webhookId == null || forceRegister) {
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
var registrationData = _getAppRegistrationData();
registrationData.addAll({
"app_id": "ha_client",
"app_name": "$appName",
"supports_encryption": false,
});
Connection().sendHTTPPost(
endPoint: "/api/mobile_app/registrations",
includeAuthHeader: true,
data: json.encode(registrationData)
).then((response) {
Logger.d("Processing registration responce...");
var responseObject = json.decode(response);
SharedPreferences.getInstance().then((prefs) {
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
Connection().webhookId = responseObject["webhook_id"];
completer.complete();
eventBus.fire(ShowDialogEvent(
title: "Mobile app Integration was created",
body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant",
positiveText: "Restart now",
negativeText: "Later",
onPositive: () {
Connection().callService(domain: "homeassistant", service: "restart", entityId: null);
},
));
});
}).catchError((e) {
completer.complete();
Logger.e("Error registering the app: ${e.toString()}");
});
return completer.future;
} else {
Logger.w("Unknown message type: $message");
Logger.d("App was previously registered. Checking...");
var updateData = {
"type": "update_registration",
"data": _getAppRegistrationData()
};
Connection().sendHTTPPost(
endPoint: "/api/webhook/${Connection().webhookId}",
includeAuthHeader: false,
data: json.encode(updateData)
).then((response) {
Logger.d("App registration works fine");
if (showOkDialog) {
eventBus.fire(ShowDialogEvent(
title: "All good",
body: "HA Client integration with your Home Assistant server works fine",
positiveText: "Nice!",
negativeText: "Ok"
));
}
completer.complete();
}).catchError((e) {
if (e['code'] != null && e['code'] == 410) {
Logger.e("MobileApp integration was removed");
eventBus.fire(ShowDialogEvent(
title: "App integration was removed",
body: "Looks like app integration was removed from your Home Assistant. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and other useful stuff.",
positiveText: "Register now",
negativeText: "Cancel",
onPositive: () {
SharedPreferences.getInstance().then((prefs) {
prefs.remove("app-webhook-id");
Connection().webhookId = null;
HomeAssistant().checkAppRegistration();
});
},
));
} else {
Logger.e("Error updating app registration: ${e.toString()}");
eventBus.fire(ShowDialogEvent(
title: "App integration is not working properly",
body: "Something wrong with HA Client integration on your Home Assistant server. Try to remove current app integration from Configuration -> Integrationds using web UI, restart your Home Assistant and go back to the app. NOTE that after clicking 'Ok' current integration data will be removed from the app and new integration wll be created on Home Assistant side on next app launch.",
positiveText: "Ok",
negativeText: "I'll handle it",
onPositive: () {
SharedPreferences.getInstance().then((prefs) {
prefs.remove("app-webhook-id");
Connection().webhookId = null;
HAUtils.launchURL(Connection().httpWebHost+"/config/integrations/dashboard");
});
},
));
}
completer.complete();
});
return completer.future;
}
}
void _sendSubscribe() {
_incrementMessageId();
_subscriptionMessageId = _currentMessageId;
_send('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
}
Future _getConfig() async {
await _sendInitialMessage("get_config").then((data) => _instanceConfig = Map.from(data["result"]));
await Connection().sendSocketMessage(type: "get_config").then((data) {
_instanceConfig = Map.from(data);
}).catchError((e) {
throw HAError("Error getting config: ${e}");
});
}
Future _getStates() async {
await _sendInitialMessage("get_states").then((data) => entities.parse(data["result"]));
await Connection().sendSocketMessage(type: "get_states").then(
(data) => entities.parse(data)
).catchError((e) {
throw HAError("Error getting states: $e");
});
}
Future _getLovelace() async {
await _sendInitialMessage("lovelace/config").then((data) => _rawLovelaceData = data["result"]);
await Connection().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
throw HAError("Error getting lovelace config: $e");
});
}
Future _getUserInfo() async {
_userName = null;
await _sendInitialMessage("auth/current_user").then((data) => _userName = data["result"]["name"]);
await Connection().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
Logger.w("Can't get user info: ${e}");
});
}
Future _getServices() async {
await _sendInitialMessage("get_services").then((data) => Logger.d("We actually don`t need the list of servcies for now"));
await Connection().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
Logger.w("Can't get services: ${e}");
});
}
Future _getPanels() async {
panels.clear();
await _sendInitialMessage("get_panels").then((data) {
if (data["success"]) {
data["result"].forEach((k,v) {
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
panels.add(Panel(
id: k,
type: v["component_name"],
title: title,
urlPath: v["url_path"],
config: v["config"],
icon: v["icon"]
)
);
});
}
});
}
_incrementMessageId() {
_currentMessageId += 1;
}
void _sendAuthMessage(String message) {
Logger.d( "[Sending] ==> auth request");
_hassioChannel.sink.add(message);
}
Future _sendInitialMessage(String type) {
Completer _completer = Completer();
_incrementMessageId();
_messageResolver[_currentMessageId] = _completer;
_send('{"id": $_currentMessageId, "type": "$type"}', false);
return _completer.future;
}
_send(String message, bool queued) {
var sendCompleter = Completer();
if (queued) _messageQueue.add(message);
_connection().then((r) {
_messageQueue.getActualMessages().forEach((message){
Logger.d( "[Sending queued] ==> $message");
_hassioChannel.sink.add(message);
await Connection().sendSocketMessage(type: "get_panels").then((data) {
data.forEach((k,v) {
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
panels.add(Panel(
id: k,
type: v["component_name"],
title: title,
urlPath: v["url_path"],
config: v["config"],
icon: v["icon"]
)
);
});
if (!queued) {
Logger.d( "[Sending] ==> $message");
_hassioChannel.sink.add(message);
}
sendCompleter.complete();
}).catchError((e){
sendCompleter.completeError(e);
}).catchError((e) {
throw HAError("Error getting panels list: $e");
});
return sendCompleter.future;
}
Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
_incrementMessageId();
String message = "";
if (entityId != null) {
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
if (additionalParams != null) {
additionalParams.forEach((name, value) {
if ((value is double) || (value is int) || (value is List)) {
message += ', "$name" : $value';
} else {
message += ', "$name" : "$value"';
}
});
}
message += '}}';
} else {
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service"';
if (additionalParams != null && additionalParams.isNotEmpty) {
message += ', "service_data": {';
bool first = true;
additionalParams.forEach((name, value) {
if (!first) {
message += ', ';
}
if ((value is double) || (value is int) || (value is List)) {
message += '"$name" : $value';
} else {
message += '"$name" : "$value"';
}
first = false;
});
message += '}';
}
message += '}';
}
return _send(message, true);
}
void _handleEntityStateChange(Map eventData) {
//TheLogger.debug( "New state for ${eventData['entity_id']}");
Map data = Map.from(eventData);
eventBus.fire(new StateChangedEvent(
entityId: data["entity_id"],
needToRebuildUI: entities.updateState(data)
));
if (_fetchCompleter.isCompleted) {
Map data = Map.from(eventData);
eventBus.fire(new StateChangedEvent(
entityId: data["entity_id"],
needToRebuildUI: entities.updateState(data)
));
}
}
void _parseLovelace() {
@ -521,7 +421,7 @@ class HomeAssistant {
void _createUI() {
ui = HomeAssistantUI();
if ((_useLovelace) && (_rawLovelaceData != null)) {
if ((Connection().useLovelace) && (_rawLovelaceData != null)) {
Logger.d("Creating Lovelace UI");
_parseLovelace();
} else {
@ -555,31 +455,12 @@ class HomeAssistant {
}
}
Widget buildViews(BuildContext context, bool lovelace, TabController tabController) {
Widget buildViews(BuildContext context, TabController tabController) {
return ui.build(context, tabController);
}
Future<List> getHistory(String entityId) async {
DateTime now = DateTime.now();
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
Logger.d("[Sending] ==> $url");
http.Response historyResponse;
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_password",
"Content-Type": "application/json"
});
var history = json.decode(historyResponse.body);
if (history is List) {
Logger.d( "[Received] <== ${history.first.length} history recors");
return history;
} else {
return [];
}
}
}
/*
class SendMessageQueue {
int _messageTimeout;
List<HAMessage> _queue = [];
@ -618,4 +499,4 @@ class HAMessage {
bool isExpired() {
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
}
}
}*/

View File

@ -15,8 +15,13 @@ import 'package:http/http.dart' as http;
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:progress_indicators/progress_indicators.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_svg/flutter_svg.dart';
//import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:device_info/device_info.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
part 'entity_class/const.dart';
part 'entity_class/entity.class.dart';
@ -87,6 +92,9 @@ part 'entity.page.dart';
part 'utils.class.dart';
part 'mdi.class.dart';
part 'entity_collection.class.dart';
part 'auth_manager.class.dart';
part 'connection.class.dart';
part 'device.class.dart';
part 'ui_class/ui.dart';
part 'ui_class/view.class.dart';
part 'ui_class/card.class.dart';
@ -99,10 +107,10 @@ part 'ui_widgets/config_panel_widget.dart';
EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = "HA Client";
const appVersion = "0.5.0";
String homeAssistantWebHost;
const appVersion = "0.6.0-alpha3";
void main() {
FlutterError.onError = (errorDetails) {
@ -124,6 +132,8 @@ void main() {
}
class HAClientApp extends StatelessWidget {
final HomeAssistant homeAssistant = HomeAssistant();
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
@ -134,106 +144,186 @@ class HAClientApp extends StatelessWidget {
),
initialRoute: "/",
routes: {
"/": (context) => MainPage(title: 'HA Client'),
"/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
"/configuration": (context) => PanelPage(title: "Configuration"),
"/log-view": (context) => LogViewPage(title: "Log")
"/log-view": (context) => LogViewPage(title: "Log"),
"/login": (_) => WebviewScaffold(
url: "${Connection().oauthUrl}",
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.help),
onPressed: () => HAUtils.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#authentication")
),
title: new Text("Login to your Home Assistant"),
),
)
},
);
}
}
class MainPage extends StatefulWidget {
MainPage({Key key, this.title}) : super(key: key);
MainPage({Key key, this.title, this.homeAssistant}) : super(key: key);
final String title;
final HomeAssistant homeAssistant;
@override
_MainPageState createState() => new _MainPageState();
}
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
HomeAssistant _homeAssistant;
//Map _instanceConfig;
String _webSocketApiEndpoint;
String _password;
//int _uiViewsCount = 0;
String _instanceHost;
StreamSubscription _stateSubscription;
StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription;
StreamSubscription _showErrorSubscription;
bool _settingsLoaded = false;
bool _accountMenuExpanded = false;
bool _useLovelaceUI;
StreamSubscription _startAuthSubscription;
StreamSubscription _showDialogSubscription;
StreamSubscription _reloadUISubscription;
int _previousViewCount;
bool _showLoginButton = false;
@override
void initState() {
super.initState();
_settingsLoaded = false;
WidgetsBinding.instance.addObserver(this);
Logger.d("<!!!> Creating new HomeAssistant instance");
_homeAssistant = HomeAssistant();
_firebaseMessaging.configure(
onLaunch: (data) {
Logger.d("Notification [onLaunch]: $data");
},
onMessage: (data) {
Logger.d("Notification [onMessage]: $data");
_showNotification(title: data["notification"]["title"], text: data["notification"]["body"]);
},
onResume: (data) {
Logger.d("Notification [onResume]: $data");
}
);
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
var initializationSettingsAndroid =
new AndroidInitializationSettings('mini_icon');
var initializationSettingsIOS = new IOSInitializationSettings(
onDidReceiveLocalNotification: null);
var initializationSettings = new InitializationSettings(
initializationSettingsAndroid, initializationSettingsIOS);
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: onSelectNotification);
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
Logger.d("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) {
_homeAssistant.disconnect().then((_){
_initialLoad();
});
_fullLoad();
}
});
_initialLoad();
_fullLoad();
}
void _initialLoad() {
_loadConnectionSettings().then((_){
_subscribe();
_refreshData();
}, onError: (_) {
_showErrorBottomBar(message: _, errorCode: 5);
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() async {
_showInfoBottomBar(progress: true,);
_subscribe().then((_) {
Connection().init(loadSettings: true, forceReconnect: true).then((__){
_fetchData();
}, onError: (e) {
_setErrorState(e);
});
});
}
void _quickLoad() {
_hideBottomBar();
_showInfoBottomBar(progress: true,);
Connection().init(loadSettings: false, forceReconnect: false).then((_){
_fetchData();
}, onError: (e) {
_setErrorState(e);
});
}
_fetchData() async {
await widget.homeAssistant.fetchData().then((_) {
_hideBottomBar();
int currentViewCount = widget.homeAssistant.ui?.views?.length ?? 0;
if (_previousViewCount != currentViewCount) {
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
_viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount;
}
}).catchError((e) {
if (e is HAError) {
_setErrorState(e);
} else {
_setErrorState(HAError(e.toString()));
}
});
eventBus.fire(RefreshDataFinishedEvent());
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
Logger.d("$state");
if (state == AppLifecycleState.resumed && _settingsLoaded) {
_refreshData();
if (state == AppLifecycleState.resumed && Connection().settingsLoaded) {
_quickLoad();
}
}
_loadConnectionSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String domain = prefs.getString('hassio-domain');
String port = prefs.getString('hassio-port');
_instanceHost = "$domain:$port";
_webSocketApiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
_password = prefs.getString('hassio-password');
_useLovelaceUI = prefs.getBool('use-lovelace') ?? true;
if ((domain == null) || (port == null) || (_password == null) ||
(domain.length == 0) || (port.length == 0) || (_password.length == 0)) {
throw("Check connection settings");
} else {
_settingsLoaded = true;
}
}
_subscribe() {
Future _subscribe() {
Completer completer = Completer();
if (_stateSubscription == null) {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) {
Logger.d("New entity. Need to rebuild UI");
_refreshData();
_quickLoad();
} else {
setState(() {});
}
});
}
if (_reloadUISubscription == null) {
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
_quickLoad();
});
}
if (_showDialogSubscription == null) {
_showDialogSubscription = eventBus.on<ShowDialogEvent>().listen((event){
_showDialog(
title: event.title,
body: event.body,
onPositive: event.onPositive,
onNegative: event.onNegative,
positiveText: event.positiveText,
negativeText: event.negativeText
);
});
}
if (_serviceCallSubscription == null) {
_serviceCallSubscription =
eventBus.on<ServiceCallEvent>().listen((event) {
@ -251,58 +341,95 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_showErrorBottomBar(message: event.text, errorCode: event.errorCode);
_showErrorBottomBar(event.error);
});
}
}
_refreshData() async {
_homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI);
_hideBottomBar();
_showInfoBottomBar(progress: true,);
await _homeAssistant.fetch().then((result) {
_hideBottomBar();
int currentViewCount = _homeAssistant.ui?.views?.length ?? 0;
if (_previousViewCount != currentViewCount) {
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
_viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount;
}
}).catchError((e) {
_setErrorState(e);
if (_startAuthSubscription == null) {
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
setState(() {
_showLoginButton = event.showButton;
});
if (event.showButton) {
_showOAuth();
} else {
Navigator.of(context).pop();
}
});
}
_firebaseMessaging.getToken().then((String token) {
HomeAssistant().fcmToken = token;
completer.complete();
});
eventBus.fire(RefreshDataFinishedEvent());
return completer.future;
}
_setErrorState(e) {
if (e is Error) {
Logger.e(e.toString());
Logger.e("${e.stackTrace}");
void _showOAuth() {
Logger.d("_showOAuth: ${Connection().oauthUrl}");
Navigator.of(context).pushNamed('/login');
}
_setErrorState(HAError e) {
if (e == null) {
_showErrorBottomBar(
message: "There was some error",
errorCode: 13
HAError("Unknown error")
);
} else {
_showErrorBottomBar(
message: e != null ? e["errorMessage"] ?? "$e" : "Unknown error",
errorCode: e["errorCode"] != null ? e["errorCode"] : 99
);
_showErrorBottomBar(e);
}
}
void _callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
void _showDialog({String title, String body, var onPositive, var onNegative, String positiveText, String negativeText}) {
List<Widget> buttons = [];
buttons.add(FlatButton(
child: new Text("$positiveText"),
onPressed: () {
Navigator.of(context).pop();
if (onPositive != null) {
onPositive();
}
},
));
if (negativeText != null) {
buttons.add(FlatButton(
child: new Text("$negativeText"),
onPressed: () {
Navigator.of(context).pop();
if (onNegative != null) {
onNegative();
}
},
));
}
// flutter defined function
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
title: new Text("$title"),
content: new Text("$body"),
actions: buttons,
);
},
);
}
void _callService(String domain, String service, String entityId, Map additionalParams) {
_showInfoBottomBar(
message: "Calling $domain.$service",
duration: Duration(seconds: 3)
);
_homeAssistant.callService(domain, service, entityId, additionalParams).catchError((e) => _setErrorState(e));
Connection().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
}
void _showEntityPage(String entityId) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: _homeAssistant),
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: widget.homeAssistant),
)
);
}
@ -310,8 +437,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
List<Tab> buildUIViewTabs() {
List<Tab> result = [];
if (_homeAssistant.ui.views.isNotEmpty) {
_homeAssistant.ui.views.forEach((HAView view) {
if (widget.homeAssistant.ui.views.isNotEmpty) {
widget.homeAssistant.ui.views.forEach((HAView view) {
result.add(view.buildTab());
});
}
@ -323,16 +450,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
List<Widget> menuItems = [];
menuItems.add(
UserAccountsDrawerHeader(
accountName: Text(_homeAssistant.userName),
accountEmail: Text(_instanceHost ?? "Not configured"),
onDetailsPressed: () {
accountName: Text(widget.homeAssistant.userName),
accountEmail: Text(Connection().displayHostname ?? "Not configured"),
/*onDetailsPressed: () {
setState(() {
_accountMenuExpanded = !_accountMenuExpanded;
});
},
},*/
currentAccountPicture: CircleAvatar(
child: Text(
_homeAssistant.userAvatarText,
widget.homeAssistant.userAvatarText,
style: TextStyle(
fontSize: 32.0
),
@ -340,41 +467,43 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
),
)
);
if (_accountMenuExpanded) {
menuItems.addAll([
ListTile(
leading: Icon(Icons.settings),
title: Text("Settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/connection-settings');
},
),
Divider(),
]);
} else {
if (_homeAssistant != null && _homeAssistant.panels.isNotEmpty) {
_homeAssistant.panels.forEach((Panel panel) {
if (widget.homeAssistant.panels.isNotEmpty) {
widget.homeAssistant.panels.forEach((Panel panel) {
if (!panel.isHidden) {
menuItems.add(
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
title: Text("${panel.title}"),
onTap: () => panel.handleOpen(context)
onTap: () {
Navigator.of(context).pop();
panel.handleOpen(context);
}
)
);
}
});
menuItems.addAll([
}
//TODO check for loaded
menuItems.add(
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
title: Text("Open Web UI"),
onTap: () => HAUtils.launchURL(homeAssistantWebHost),
),
Divider()
]);
}
onTap: () => HAUtils.launchURL(Connection().httpWebHost),
)
);
menuItems.addAll([
Divider(),
ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
title: Text("Connection settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/connection-settings', arguments: {"homeAssistant", widget.homeAssistant});
},
)
]);
menuItems.addAll([
Divider(),
new ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text("Log"),
@ -392,6 +521,22 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
},
),
Divider(),
new ListTile(
leading: Icon(Icons.help),
title: Text("Help"),
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURL("http://ha-client.homemade.systems/docs");
},
),
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
title: Text("Join Discord channel"),
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURL("https://discord.gg/AUzEvwn");
},
),
new AboutListTile(
aboutBoxChildren: <Widget>[
GestureDetector(
@ -406,13 +551,44 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
decoration: TextDecoration.underline
),
),
),
Container(
height: 10.0,
),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions");
},
child: Text(
"Terms and Conditions",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
),
),
),
Container(
height: 10.0,
),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy");
},
child: Text(
"Privacy Policy",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
),
),
)
],
applicationName: appName,
applicationVersion: appVersion
)
]);
}
return new Drawer(
child: ListView(
children: menuItems,
@ -450,101 +626,100 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}
}
void _showErrorBottomBar({Key key, @required String message, @required int errorCode}) {
void _showErrorBottomBar(HAError error) {
TextStyle textStyle = TextStyle(
color: Colors.blue,
fontSize: Sizes.nameFontSize
color: Colors.blue,
fontSize: Sizes.nameFontSize
);
_bottomBarColor = Colors.red.shade100;
switch (errorCode) {
case 9:
case 11:
case 7:
case 1: {
_bottomBarAction = FlatButton(
child: Text("Retry", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
},
);
break;
}
case 5: {
message = "Check connection settings";
_bottomBarAction = FlatButton(
child: Text("Open", style: textStyle),
List<Widget> actions = [];
error.actions.forEach((HAErrorAction action) {
switch (action.type) {
case HAErrorActionType.FULL_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
_fullLoad();
},
));
break;
}
case HAErrorActionType.QUICK_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
_quickLoad();
},
));
break;
}
case HAErrorActionType.URL: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: "${action.url}");
},
));
break;
}
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
Navigator.pushNamed(context, '/connection-settings');
},
);
break;
}
case 6: {
_bottomBarAction = FlatButton(
child: Text("Settings", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
Navigator.pushNamed(context, '/connection-settings');
},
);
break;
}
case 10: {
_bottomBarAction = FlatButton(
child: Text("Refresh", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
},
);
break;
}
case 8: {
_bottomBarAction = FlatButton(
child: Text("Reconnect", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
},
);
break;
}
default: {
_bottomBarAction = FlatButton(
child: Text("Reload", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
},
);
));
break;
}
}
setState(() {
_bottomBarProgress = false;
_bottomBarText = "$message (code: $errorCode)";
_showBottomBar = true;
});
/*_scaffoldKey.currentState.hideCurrentSnackBar();
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text("$message (code: $errorCode)"),
action: action,
duration: Duration(hours: 1),
)
);*/
});
if (actions.isNotEmpty) {
_bottomBarAction = Row(
mainAxisSize: MainAxisSize.min,
children: actions,
mainAxisAlignment: MainAxisAlignment.end,
);
} else {
_bottomBarAction = Container(height: 0.0, width: 0.0,);
}
setState(() {
_bottomBarProgress = false;
_bottomBarText = "${error.message}";
_showBottomBar = true;
});
}
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> popupMenuItems = [];
popupMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
));
List<Widget> emptyBody = [
Text("."),
];
if (Connection().isAuthenticated) {
_showLoginButton = false;
popupMenuItems.add(
PopupMenuItem<String>(
child: new Text("Logout"),
value: "logout",
));
}
if (_showLoginButton) {
emptyBody = [
FlatButton(
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
color: Colors.blue,
onPressed: () => _fullLoad(),
)
];
}
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
@ -552,7 +727,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
floating: true,
pinned: true,
primary: true,
title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""),
title: Text(widget.homeAssistant.locationName ?? ""),
actions: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
@ -561,13 +736,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
context: context,
items: [PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
)]
items: popupMenuItems
).then((String val) {
if (val == "reload") {
_refreshData();
_quickLoad();
} else if (val == "logout") {
widget.homeAssistant.logout().then((_) {
_quickLoad();
});
}
});
}
@ -577,9 +753,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
setState(() {
_accountMenuExpanded = false;
});
},
),
bottom: empty ? null : TabBar(
@ -595,17 +768,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant"),
size: 100.0,
color: Colors.blue,
),
]
children: emptyBody
),
)
:
_homeAssistant.buildViews(context, _useLovelaceUI, _viewsTabController),
widget.homeAssistant.buildViews(context, _viewsTabController),
);
}
@ -662,7 +829,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}
}
// This method is rerun every time setState is called.
if (_homeAssistant.ui == null || _homeAssistant.ui.views == null) {
if (widget.homeAssistant.isNoViews) {
return Scaffold(
key: _scaffoldKey,
primary: false,
@ -676,24 +843,27 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
drawer: _buildAppDrawer(),
primary: false,
bottomNavigationBar: bottomBar,
body: HomeAssistantModel(
child: _buildScaffoldBody(false),
homeAssistant: _homeAssistant
),
body: _buildScaffoldBody(false),
);
}
}
@override
void dispose() {
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.dispose();
WidgetsBinding.instance.removeObserver(this);
_viewsTabController.dispose();
if (_stateSubscription != null) _stateSubscription.cancel();
if (_settingsSubscription != null) _settingsSubscription.cancel();
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
if (_showErrorSubscription != null) _showErrorSubscription.cancel();
_homeAssistant.disconnect();
_viewsTabController?.dispose();
_stateSubscription?.cancel();
_settingsSubscription?.cancel();
_serviceCallSubscription?.cancel();
_showDialogSubscription?.cancel();
_showEntityPageSubscription?.cancel();
_showErrorSubscription?.cancel();
_startAuthSubscription?.cancel();
_reloadUISubscription?.cancel();
//TODO disconnect
//widget.homeAssistant?.disconnect();
super.dispose();
}
}

View File

@ -24,10 +24,12 @@ class MaterialDesignIcons {
"cover.opening": "mdi:window-open",
"camera": "mdi:cctv",
"calendar": "mdi:calendar",
"device_tracker": "mdi:account",
"timer": "mdi:timer",
"lock.locked": "mdi:lock",
"lock.unlocked": "mdi:lock-open",
"fan": "mdi:fan",
"remote": "mdi:remote",
"alarm_control_panel.disarmed" : "mdi:bell-outline",
"alarm_control_panel.armed_home" : "mdi:bell-plus",
"alarm_control_panel.armed_away" : "mdi:bell",
@ -64,18 +66,18 @@ class MaterialDesignIcons {
"binary_sensor.occupancy.off": "mdi:home-outline",
"binary_sensor.opening.on": "mdi:square-outline",
"binary_sensor.opening.off": "mdi:square",
//"binary_sensor.plug.on": "mdi:",
//"binary_sensor.plug.off": "mdi:",
"binary_sensor.plug.on": "mdi:power-plug",
"binary_sensor.plug.off": "mdi:power-plug-off",
"binary_sensor.power.on": "mdi:alert",
"binary_sensor.power.off": "mdi:verified",
"binary_sensor.power.off": "mdi:shield-check",
//"binary_sensor.presence.on": "mdi:",
//"binary_sensor.presence.off": "mdi:",
"binary_sensor.problem.on": "mdi:alert-outline",
"binary_sensor.problem.off": "mdi:check-outline",
"binary_sensor.safety.on": "mdi:alert",
"binary_sensor.safety.off": "mdi:verified",
"binary_sensor.safety.off": "mdi:shield-check",
"binary_sensor.smoke.on": "mdi:alert",
"binary_sensor.smoke.off": "mdi:verified",
"binary_sensor.smoke.off": "mdi:shield-check",
"binary_sensor.sound.on": "mdi:music-note",
"binary_sensor.sound.off": "mdi:music-note-off",
"binary_sensor.vibration.on": "mdi:vibrate",
@ -96,8 +98,8 @@ class MaterialDesignIcons {
"cover.window.closing": "mdi:window-open",
"cover.window.opening": "mdi:window-open",
};
static final Map iconsDataMap = {
static final Map iconsDataMap = {
"mdi:access-point": 0xf002,
"mdi:access-point-network": 0xf003,
"mdi:access-point-network-off": 0xfbbd,
@ -111,6 +113,8 @@ class MaterialDesignIcons {
"mdi:account-badge": 0xfd83,
"mdi:account-badge-alert": 0xfd84,
"mdi:account-badge-alert-outline": 0xfd85,
"mdi:account-badge-horizontal": 0xfdf0,
"mdi:account-badge-horizontal-outline": 0xfdf1,
"mdi:account-badge-outline": 0xfd86,
"mdi:account-box": 0xf006,
"mdi:account-box-multiple": 0xf933,
@ -492,6 +496,7 @@ class MaterialDesignIcons {
"mdi:auto-upload": 0xf069,
"mdi:autorenew": 0xf06a,
"mdi:av-timer": 0xf06b,
"mdi:aws": 0xfdf2,
"mdi:axe": 0xf8c7,
"mdi:axis": 0xfd24,
"mdi:axis-arrow": 0xfd25,
@ -519,6 +524,10 @@ class MaterialDesignIcons {
"mdi:backspace-outline": 0xfb38,
"mdi:backup-restore": 0xf06f,
"mdi:badminton": 0xf850,
"mdi:bag-personal": 0xfdf3,
"mdi:bag-personal-off": 0xfdf4,
"mdi:bag-personal-off-outline": 0xfdf5,
"mdi:bag-personal-outline": 0xfdf6,
"mdi:balloon": 0xfa25,
"mdi:ballot": 0xf9c8,
"mdi:ballot-outline": 0xf9c9,
@ -626,6 +635,7 @@ class MaterialDesignIcons {
"mdi:bell-sleep-outline": 0xfa92,
"mdi:beta": 0xf0a1,
"mdi:betamax": 0xf9ca,
"mdi:biathlon": 0xfdf7,
"mdi:bible": 0xf0a2,
"mdi:bike": 0xf0a3,
"mdi:billiards": 0xfb3d,
@ -680,6 +690,8 @@ class MaterialDesignIcons {
"mdi:bookmark-check": 0xf0c1,
"mdi:bookmark-minus": 0xf9cb,
"mdi:bookmark-minus-outline": 0xf9cc,
"mdi:bookmark-multiple": 0xfdf8,
"mdi:bookmark-multiple-outline": 0xfdf9,
"mdi:bookmark-music": 0xf0c2,
"mdi:bookmark-off": 0xf9cd,
"mdi:bookmark-off-outline": 0xf9ce,
@ -791,6 +803,8 @@ class MaterialDesignIcons {
"mdi:calendar-heart": 0xf9d1,
"mdi:calendar-import": 0xfb0a,
"mdi:calendar-minus": 0xfd38,
"mdi:calendar-month": 0xfdfa,
"mdi:calendar-month-outline": 0xfdfb,
"mdi:calendar-multiple": 0xf0f1,
"mdi:calendar-multiple-check": 0xf0f2,
"mdi:calendar-multiselect": 0xfa31,
@ -837,6 +851,8 @@ class MaterialDesignIcons {
"mdi:camera-party-mode": 0xf105,
"mdi:camera-rear": 0xf106,
"mdi:camera-rear-variant": 0xf107,
"mdi:camera-retake": 0xfdfc,
"mdi:camera-retake-outline": 0xfdfd,
"mdi:camera-switch": 0xf108,
"mdi:camera-timer": 0xf109,
"mdi:camera-wireless": 0xfd92,
@ -847,6 +863,7 @@ class MaterialDesignIcons {
"mdi:cannabis": 0xf7a5,
"mdi:caps-lock": 0xfa9a,
"mdi:car": 0xf10b,
"mdi:car-back": 0xfdfe,
"mdi:car-battery": 0xf10c,
"mdi:car-brake-abs": 0xfc23,
"mdi:car-brake-alert": 0xfc24,
@ -868,6 +885,7 @@ class MaterialDesignIcons {
"mdi:car-light-high": 0xfc28,
"mdi:car-limousine": 0xf8cc,
"mdi:car-multiple": 0xfb4a,
"mdi:car-off": 0xfdff,
"mdi:car-parking-lights": 0xfd3f,
"mdi:car-pickup": 0xf7a9,
"mdi:car-side": 0xf7aa,
@ -916,6 +934,7 @@ class MaterialDesignIcons {
"mdi:cassette": 0xf9d3,
"mdi:cast": 0xf118,
"mdi:cast-connected": 0xf119,
"mdi:cast-education": 0xfe6d,
"mdi:cast-off": 0xf789,
"mdi:castle": 0xf11a,
"mdi:cat": 0xf11b,
@ -966,6 +985,7 @@ class MaterialDesignIcons {
"mdi:chat-processing": 0xfb57,
"mdi:check": 0xf12c,
"mdi:check-all": 0xf12d,
"mdi:check-bold": 0xfe6e,
"mdi:check-box-multiple-outline": 0xfc2d,
"mdi:check-box-outline": 0xfc2e,
"mdi:check-circle": 0xf5e0,
@ -974,6 +994,9 @@ class MaterialDesignIcons {
"mdi:check-network": 0xfc2f,
"mdi:check-network-outline": 0xfc30,
"mdi:check-outline": 0xf854,
"mdi:check-underline": 0xfe70,
"mdi:check-underline-circle": 0xfe71,
"mdi:check-underline-circle-outline": 0xfe72,
"mdi:checkbook": 0xfa9c,
"mdi:checkbox-blank": 0xf12e,
"mdi:checkbox-blank-circle": 0xf12f,
@ -1049,6 +1072,7 @@ class MaterialDesignIcons {
"mdi:circle-slice-7": 0xfaa3,
"mdi:circle-slice-8": 0xfaa4,
"mdi:circle-small": 0xf9de,
"mdi:circular-saw": 0xfe73,
"mdi:cisco-webex": 0xf145,
"mdi:city": 0xf146,
"mdi:city-variant": 0xfa35,
@ -1148,6 +1172,11 @@ class MaterialDesignIcons {
"mdi:collapse-all": 0xfaa5,
"mdi:collapse-all-outline": 0xfaa6,
"mdi:color-helper": 0xf179,
"mdi:comma": 0xfe74,
"mdi:comma-box": 0xfe75,
"mdi:comma-box-outline": 0xfe76,
"mdi:comma-circle": 0xfe77,
"mdi:comma-circle-outline": 0xfe78,
"mdi:comment": 0xf17a,
"mdi:comment-account": 0xf17b,
"mdi:comment-account-outline": 0xf17c,
@ -1201,6 +1230,8 @@ class MaterialDesignIcons {
"mdi:content-save-all": 0xf194,
"mdi:content-save-edit": 0xfcd7,
"mdi:content-save-edit-outline": 0xfcd8,
"mdi:content-save-move": 0xfe79,
"mdi:content-save-move-outline": 0xfe7a,
"mdi:content-save-outline": 0xf817,
"mdi:content-save-settings": 0xf61b,
"mdi:content-save-settings-outline": 0xfb13,
@ -1546,6 +1577,7 @@ class MaterialDesignIcons {
"mdi:file-cancel-outline": 0xfda3,
"mdi:file-chart": 0xf215,
"mdi:file-check": 0xf216,
"mdi:file-check-outline": 0xfe7b,
"mdi:file-cloud": 0xf217,
"mdi:file-compare": 0xf8a9,
"mdi:file-delimited": 0xf218,
@ -1573,9 +1605,11 @@ class MaterialDesignIcons {
"mdi:file-move": 0xfab8,
"mdi:file-multiple": 0xf222,
"mdi:file-music": 0xf223,
"mdi:file-music-outline": 0xfe7c,
"mdi:file-outline": 0xf224,
"mdi:file-pdf": 0xf225,
"mdi:file-pdf-box": 0xf226,
"mdi:file-pdf-outline": 0xfe7d,
"mdi:file-percent": 0xf81d,
"mdi:file-plus": 0xf751,
"mdi:file-powerpoint": 0xf227,
@ -1596,6 +1630,7 @@ class MaterialDesignIcons {
"mdi:file-upload": 0xfa4c,
"mdi:file-upload-outline": 0xfa4d,
"mdi:file-video": 0xf22b,
"mdi:file-video-outline": 0xfe10,
"mdi:file-word": 0xf22c,
"mdi:file-word-box": 0xf22d,
"mdi:file-xml": 0xf22e,
@ -1614,6 +1649,9 @@ class MaterialDesignIcons {
"mdi:fire-truck": 0xf8aa,
"mdi:firebase": 0xf966,
"mdi:firefox": 0xf239,
"mdi:fireplace": 0xfe11,
"mdi:fireplace-off": 0xfe12,
"mdi:firework": 0xfe13,
"mdi:fish": 0xf23a,
"mdi:flag": 0xf23b,
"mdi:flag-checkered": 0xf23c,
@ -1715,6 +1753,7 @@ class MaterialDesignIcons {
"mdi:format-bold": 0xf264,
"mdi:format-clear": 0xf265,
"mdi:format-color-fill": 0xf266,
"mdi:format-color-highlight": 0xfe14,
"mdi:format-color-text": 0xf69d,
"mdi:format-columns": 0xf8de,
"mdi:format-float-center": 0xf267,
@ -1769,6 +1808,7 @@ class MaterialDesignIcons {
"mdi:format-text": 0xf284,
"mdi:format-text-rotation-down": 0xfd4f,
"mdi:format-text-rotation-none": 0xfd50,
"mdi:format-text-variant": 0xfe15,
"mdi:format-text-wrapping-clip": 0xfcea,
"mdi:format-text-wrapping-overflow": 0xfceb,
"mdi:format-text-wrapping-wrap": 0xfcec,
@ -1805,6 +1845,22 @@ class MaterialDesignIcons {
"mdi:fuse": 0xfc61,
"mdi:fuse-blade": 0xfc62,
"mdi:gamepad": 0xf296,
"mdi:gamepad-circle": 0xfe16,
"mdi:gamepad-circle-down": 0xfe17,
"mdi:gamepad-circle-left": 0xfe18,
"mdi:gamepad-circle-outline": 0xfe19,
"mdi:gamepad-circle-right": 0xfe1a,
"mdi:gamepad-circle-up": 0xfe1b,
"mdi:gamepad-down": 0xfe1c,
"mdi:gamepad-left": 0xfe1d,
"mdi:gamepad-right": 0xfe1e,
"mdi:gamepad-round": 0xfe1f,
"mdi:gamepad-round-down": 0xfe7e,
"mdi:gamepad-round-left": 0xfe7f,
"mdi:gamepad-round-outline": 0xfe80,
"mdi:gamepad-round-right": 0xfe81,
"mdi:gamepad-round-up": 0xfe82,
"mdi:gamepad-up": 0xfe83,
"mdi:gamepad-variant": 0xf297,
"mdi:gantry-crane": 0xfdad,
"mdi:garage": 0xf6d8,
@ -1820,6 +1876,7 @@ class MaterialDesignIcons {
"mdi:gate-or": 0xf8e4,
"mdi:gate-xnor": 0xf8e5,
"mdi:gate-xor": 0xf8e6,
"mdi:gatsby": 0xfe84,
"mdi:gauge": 0xf29a,
"mdi:gauge-empty": 0xf872,
"mdi:gauge-full": 0xf873,
@ -1848,7 +1905,8 @@ class MaterialDesignIcons {
"mdi:ghost": 0xf2a0,
"mdi:ghost-off": 0xf9f4,
"mdi:gif": 0xfd54,
"mdi:gift": 0xf2a1,
"mdi:gift": 0xfe85,
"mdi:gift-outline": 0xf2a1,
"mdi:git": 0xf2a2,
"mdi:github-box": 0xf2a3,
"mdi:github-circle": 0xf2a4,
@ -1915,6 +1973,7 @@ class MaterialDesignIcons {
"mdi:grid": 0xf2c1,
"mdi:grid-large": 0xf757,
"mdi:grid-off": 0xf2c2,
"mdi:grill": 0xfe86,
"mdi:group": 0xf2c3,
"mdi:guitar-acoustic": 0xf770,
"mdi:guitar-electric": 0xf2c4,
@ -1927,6 +1986,7 @@ class MaterialDesignIcons {
"mdi:hamburger": 0xf684,
"mdi:hammer": 0xf8e9,
"mdi:hand": 0xfa4e,
"mdi:hand-left": 0xfe87,
"mdi:hand-okay": 0xfa4f,
"mdi:hand-peace": 0xfa50,
"mdi:hand-peace-variant": 0xfa51,
@ -1934,6 +1994,8 @@ class MaterialDesignIcons {
"mdi:hand-pointing-left": 0xfa53,
"mdi:hand-pointing-right": 0xf2c7,
"mdi:hand-pointing-up": 0xfa54,
"mdi:hand-right": 0xfe88,
"mdi:hand-saw": 0xfe89,
"mdi:hanger": 0xf2c8,
"mdi:hard-hat": 0xf96e,
"mdi:harddisk": 0xf2ca,
@ -2070,6 +2132,7 @@ class MaterialDesignIcons {
"mdi:image-filter-none": 0xf2f6,
"mdi:image-filter-tilt-shift": 0xf2f7,
"mdi:image-filter-vintage": 0xf2f8,
"mdi:image-frame": 0xfe8a,
"mdi:image-move": 0xf9f7,
"mdi:image-multiple": 0xf2f9,
"mdi:image-off": 0xf82a,
@ -2095,6 +2158,7 @@ class MaterialDesignIcons {
"mdi:instapaper": 0xf2ff,
"mdi:internet-explorer": 0xf300,
"mdi:invert-colors": 0xf301,
"mdi:invert-colors-off": 0xfe8b,
"mdi:ip": 0xfa5e,
"mdi:ip-network": 0xfa5f,
"mdi:ip-network-outline": 0xfc6c,
@ -2124,6 +2188,7 @@ class MaterialDesignIcons {
"mdi:keyboard-caps": 0xf30e,
"mdi:keyboard-close": 0xf30f,
"mdi:keyboard-off": 0xf310,
"mdi:keyboard-off-outline": 0xfe8c,
"mdi:keyboard-outline": 0xf97a,
"mdi:keyboard-return": 0xf311,
"mdi:keyboard-settings": 0xf9f8,
@ -2175,9 +2240,12 @@ class MaterialDesignIcons {
"mdi:launch": 0xf327,
"mdi:lava-lamp": 0xf7d4,
"mdi:layers": 0xf328,
"mdi:layers-minus": 0xfe8d,
"mdi:layers-off": 0xf329,
"mdi:layers-off-outline": 0xf9fc,
"mdi:layers-outline": 0xf9fd,
"mdi:layers-plus": 0xfe30,
"mdi:layers-remove": 0xfe31,
"mdi:lead-pencil": 0xf64f,
"mdi:leaf": 0xf32a,
"mdi:leaf-maple": 0xfc6f,
@ -2202,6 +2270,8 @@ class MaterialDesignIcons {
"mdi:lifebuoy": 0xf87d,
"mdi:light-switch": 0xf97d,
"mdi:lightbulb": 0xf335,
"mdi:lightbulb-off": 0xfe32,
"mdi:lightbulb-off-outline": 0xfe33,
"mdi:lightbulb-on": 0xf6e7,
"mdi:lightbulb-on-outline": 0xf6e8,
"mdi:lightbulb-outline": 0xf336,
@ -2374,6 +2444,7 @@ class MaterialDesignIcons {
"mdi:monitor-lock": 0xfdb7,
"mdi:monitor-multiple": 0xf37a,
"mdi:monitor-off": 0xfd6c,
"mdi:monitor-screenshot": 0xfe34,
"mdi:monitor-star": 0xfdb8,
"mdi:more": 0xf37b,
"mdi:mother-nurse": 0xfcfd,
@ -2437,8 +2508,11 @@ class MaterialDesignIcons {
"mdi:new-box": 0xf394,
"mdi:newspaper": 0xf395,
"mdi:nfc": 0xf396,
"mdi:nfc-off": 0xfe35,
"mdi:nfc-search-variant": 0xfe36,
"mdi:nfc-tap": 0xf397,
"mdi:nfc-variant": 0xf398,
"mdi:nfc-variant-off": 0xfe37,
"mdi:ninja": 0xf773,
"mdi:nintendo-switch": 0xf7e0,
"mdi:nodejs": 0xf399,
@ -2452,6 +2526,7 @@ class MaterialDesignIcons {
"mdi:note-plus-outline": 0xf39d,
"mdi:note-text": 0xf39e,
"mdi:notebook": 0xf82d,
"mdi:notebook-multiple": 0xfe38,
"mdi:notification-clear-all": 0xf39f,
"mdi:npm": 0xf6f6,
"mdi:npm-variant": 0xf98e,
@ -2573,7 +2648,7 @@ class MaterialDesignIcons {
"mdi:page-previous-outline": 0xfb8f,
"mdi:palette": 0xf3d8,
"mdi:palette-advanced": 0xf3d9,
"mdi:palette-outline": 0xfde8,
"mdi:palette-outline": 0xfe6c,
"mdi:palette-swatch": 0xf8b4,
"mdi:pan": 0xfb90,
"mdi:pan-bottom-left": 0xfb91,
@ -2609,6 +2684,7 @@ class MaterialDesignIcons {
"mdi:paw": 0xf3e9,
"mdi:paw-off": 0xf657,
"mdi:paypal": 0xf882,
"mdi:pdf-box": 0xfe39,
"mdi:peace": 0xf883,
"mdi:pen": 0xf3ea,
"mdi:pen-lock": 0xfdbe,
@ -2667,6 +2743,10 @@ class MaterialDesignIcons {
"mdi:pi-hole": 0xfdcd,
"mdi:piano": 0xf67c,
"mdi:pickaxe": 0xf8b6,
"mdi:picture-in-picture-bottom-right": 0xfe3a,
"mdi:picture-in-picture-bottom-right-outline": 0xfe3b,
"mdi:picture-in-picture-top-right": 0xfe3c,
"mdi:picture-in-picture-top-right-outline": 0xfe3d,
"mdi:pier": 0xf886,
"mdi:pier-crane": 0xf887,
"mdi:pig": 0xf401,
@ -2762,7 +2842,10 @@ class MaterialDesignIcons {
"mdi:presentation-play": 0xf429,
"mdi:printer": 0xf42a,
"mdi:printer-3d": 0xf42b,
"mdi:printer-3d-nozzle": 0xfe3e,
"mdi:printer-3d-nozzle-outline": 0xfe3f,
"mdi:printer-alert": 0xf42c,
"mdi:printer-off": 0xfe40,
"mdi:printer-settings": 0xf706,
"mdi:printer-wireless": 0xfa0a,
"mdi:priority-high": 0xf603,
@ -2822,6 +2905,8 @@ class MaterialDesignIcons {
"mdi:record": 0xf44a,
"mdi:record-player": 0xf999,
"mdi:record-rec": 0xf44b,
"mdi:rectangle": 0xfe41,
"mdi:rectangle-outline": 0xfe42,
"mdi:recycle": 0xf44c,
"mdi:reddit": 0xf44d,
"mdi:redo": 0xf44e,
@ -2866,6 +2951,7 @@ class MaterialDesignIcons {
"mdi:ribbon": 0xf460,
"mdi:rice": 0xf7e9,
"mdi:ring": 0xf7ea,
"mdi:rivet": 0xfe43,
"mdi:road": 0xf461,
"mdi:road-variant": 0xf462,
"mdi:robot": 0xf6a8,
@ -2908,6 +2994,7 @@ class MaterialDesignIcons {
"mdi:satellite-uplink": 0xf908,
"mdi:satellite-variant": 0xf471,
"mdi:sausage": 0xf8b9,
"mdi:saw-blade": 0xfe44,
"mdi:saxophone": 0xf609,
"mdi:scale": 0xf472,
"mdi:scale-balance": 0xf5d1,
@ -2919,10 +3006,10 @@ class MaterialDesignIcons {
"mdi:screen-rotation": 0xf475,
"mdi:screen-rotation-lock": 0xf476,
"mdi:screw-flat-top": 0xfdcf,
"mdi:screw-lag": 0xfdd0,
"mdi:screw-machine-flat-top": 0xfdd1,
"mdi:screw-machine-round-top": 0xfdd2,
"mdi:screw-round-top": 0xfdd3,
"mdi:screw-lag": 0xfe54,
"mdi:screw-machine-flat-top": 0xfe55,
"mdi:screw-machine-round-top": 0xfe56,
"mdi:screw-round-top": 0xfe57,
"mdi:screwdriver": 0xf477,
"mdi:script": 0xfb9d,
"mdi:script-outline": 0xf478,
@ -2944,6 +3031,8 @@ class MaterialDesignIcons {
"mdi:seatbelt": 0xfca1,
"mdi:security": 0xf483,
"mdi:security-network": 0xf484,
"mdi:seed": 0xfe45,
"mdi:seed-outline": 0xfe46,
"mdi:select": 0xf485,
"mdi:select-all": 0xf486,
"mdi:select-color": 0xfd0d,
@ -2956,8 +3045,8 @@ class MaterialDesignIcons {
"mdi:selection-ellipse": 0xfd0e,
"mdi:selection-off": 0xf776,
"mdi:send": 0xf48a,
"mdi:send-circle": 0xfdd4,
"mdi:send-circle-outline": 0xfdd5,
"mdi:send-circle": 0xfe58,
"mdi:send-circle-outline": 0xfe59,
"mdi:send-lock": 0xf7ec,
"mdi:serial-port": 0xf65c,
"mdi:server": 0xf48b,
@ -3021,7 +3110,7 @@ class MaterialDesignIcons {
"mdi:ship-wheel": 0xf832,
"mdi:shoe-formal": 0xfb22,
"mdi:shoe-heel": 0xfb23,
"mdi:shoe-print": 0xfdd6,
"mdi:shoe-print": 0xfe5a,
"mdi:shopify": 0xfadd,
"mdi:shopping": 0xf49a,
"mdi:shopping-music": 0xf49b,
@ -3047,14 +3136,15 @@ class MaterialDesignIcons {
"mdi:signal-cellular-2": 0xf8bc,
"mdi:signal-cellular-3": 0xf8bd,
"mdi:signal-cellular-outline": 0xf8be,
"mdi:signal-distance-variant": 0xfe47,
"mdi:signal-hspa": 0xf714,
"mdi:signal-hspa-plus": 0xf715,
"mdi:signal-off": 0xf782,
"mdi:signal-variant": 0xf60a,
"mdi:signature": 0xfdd7,
"mdi:signature-freehand": 0xfdd8,
"mdi:signature-image": 0xfdd9,
"mdi:signature-text": 0xfdda,
"mdi:signature": 0xfe5b,
"mdi:signature-freehand": 0xfe5c,
"mdi:signature-image": 0xfe5d,
"mdi:signature-text": 0xfe5e,
"mdi:silo": 0xfb24,
"mdi:silverware": 0xf4a3,
"mdi:silverware-fork": 0xf4a4,
@ -3087,8 +3177,8 @@ class MaterialDesignIcons {
"mdi:slackware": 0xf90a,
"mdi:sleep": 0xf4b2,
"mdi:sleep-off": 0xf4b3,
"mdi:slope-downhill": 0xfddb,
"mdi:slope-uphill": 0xfddc,
"mdi:slope-downhill": 0xfe5f,
"mdi:slope-uphill": 0xfe60,
"mdi:smog": 0xfa70,
"mdi:smoke-detector": 0xf392,
"mdi:smoking": 0xf4b4,
@ -3129,6 +3219,7 @@ class MaterialDesignIcons {
"mdi:spa": 0xfcad,
"mdi:spa-outline": 0xfcae,
"mdi:space-invaders": 0xfba5,
"mdi:spade": 0xfe48,
"mdi:speaker": 0xf4c3,
"mdi:speaker-bluetooth": 0xf9a1,
"mdi:speaker-multiple": 0xfd14,
@ -3142,6 +3233,8 @@ class MaterialDesignIcons {
"mdi:spotlight-beam": 0xf4c9,
"mdi:spray": 0xf665,
"mdi:spray-bottle": 0xfadf,
"mdi:sprout": 0xfe49,
"mdi:sprout-outline": 0xfe4a,
"mdi:square": 0xf763,
"mdi:square-edit-outline": 0xf90b,
"mdi:square-inc": 0xf4ca,
@ -3246,6 +3339,7 @@ class MaterialDesignIcons {
"mdi:table-row-remove": 0xf4f5,
"mdi:table-search": 0xf90e,
"mdi:table-settings": 0xf837,
"mdi:table-tennis": 0xfe4b,
"mdi:tablet": 0xf4f6,
"mdi:tablet-android": 0xf4f7,
"mdi:tablet-cellphone": 0xf9a6,
@ -3301,12 +3395,12 @@ class MaterialDesignIcons {
"mdi:theater": 0xf50d,
"mdi:theme-light-dark": 0xf50e,
"mdi:thermometer": 0xf50f,
"mdi:thermometer-alert": 0xfddd,
"mdi:thermometer-chevron-down": 0xfdde,
"mdi:thermometer-chevron-up": 0xfddf,
"mdi:thermometer-alert": 0xfe61,
"mdi:thermometer-chevron-down": 0xfe62,
"mdi:thermometer-chevron-up": 0xfe63,
"mdi:thermometer-lines": 0xf510,
"mdi:thermometer-minus": 0xfde0,
"mdi:thermometer-plus": 0xfde1,
"mdi:thermometer-minus": 0xfe64,
"mdi:thermometer-plus": 0xfe65,
"mdi:thermostat": 0xf393,
"mdi:thermostat-box": 0xf890,
"mdi:thought-bubble": 0xf7f5,
@ -3384,12 +3478,13 @@ class MaterialDesignIcons {
"mdi:transition": 0xf914,
"mdi:transition-masked": 0xf915,
"mdi:translate": 0xf5ca,
"mdi:translate-off": 0xfde2,
"mdi:translate-off": 0xfe66,
"mdi:transmission-tower": 0xfd1a,
"mdi:trash-can": 0xfa78,
"mdi:trash-can-outline": 0xfa79,
"mdi:treasure-chest": 0xf725,
"mdi:tree": 0xf531,
"mdi:tree-outline": 0xfe4c,
"mdi:trello": 0xf532,
"mdi:trending-down": 0xf533,
"mdi:trending-neutral": 0xf534,
@ -3450,7 +3545,7 @@ class MaterialDesignIcons {
"mdi:upload-multiple": 0xf83c,
"mdi:upload-network": 0xf6f5,
"mdi:upload-network-outline": 0xfcb4,
"mdi:upload-outline": 0xfde3,
"mdi:upload-outline": 0xfe67,
"mdi:usb": 0xf553,
"mdi:van-passenger": 0xf7f9,
"mdi:van-utility": 0xf7fa,
@ -3503,6 +3598,9 @@ class MaterialDesignIcons {
"mdi:view-array": 0xf56b,
"mdi:view-carousel": 0xf56c,
"mdi:view-column": 0xf56d,
"mdi:view-comfy": 0xfe4d,
"mdi:view-compact": 0xfe4e,
"mdi:view-compact-outline": 0xfe4f,
"mdi:view-dashboard": 0xf56e,
"mdi:view-dashboard-outline": 0xfa1c,
"mdi:view-dashboard-variant": 0xf842,
@ -3537,11 +3635,12 @@ class MaterialDesignIcons {
"mdi:volume-mute": 0xf75e,
"mdi:volume-off": 0xf581,
"mdi:volume-plus": 0xf75c,
"mdi:volume-variant-off": 0xfde4,
"mdi:volume-variant-off": 0xfe68,
"mdi:vote": 0xfa1e,
"mdi:vote-outline": 0xfa1f,
"mdi:vpn": 0xf582,
"mdi:vuejs": 0xf843,
"mdi:vuetify": 0xfe50,
"mdi:walk": 0xf583,
"mdi:wall": 0xf7fd,
"mdi:wall-sconce": 0xf91b,
@ -3552,7 +3651,7 @@ class MaterialDesignIcons {
"mdi:wallet-membership": 0xf586,
"mdi:wallet-outline": 0xfbb9,
"mdi:wallet-travel": 0xf587,
"mdi:wallpaper": 0xfde5,
"mdi:wallpaper": 0xfe69,
"mdi:wan": 0xf588,
"mdi:washing-machine": 0xf729,
"mdi:watch": 0xf589,
@ -3565,13 +3664,14 @@ class MaterialDesignIcons {
"mdi:watch-vibrate-off": 0xfcb6,
"mdi:water": 0xf58c,
"mdi:water-off": 0xf58d,
"mdi:water-outline": 0xfde6,
"mdi:water-outline": 0xfe6a,
"mdi:water-percent": 0xf58e,
"mdi:water-pump": 0xf58f,
"mdi:watermark": 0xf612,
"mdi:waves": 0xf78c,
"mdi:waze": 0xfbba,
"mdi:weather-cloudy": 0xf590,
"mdi:weather-cloudy-arrow-right": 0xfe51,
"mdi:weather-fog": 0xf591,
"mdi:weather-hail": 0xf592,
"mdi:weather-hurricane": 0xf897,
@ -3608,7 +3708,7 @@ class MaterialDesignIcons {
"mdi:widgets": 0xf72b,
"mdi:wifi": 0xf5a9,
"mdi:wifi-off": 0xf5aa,
"mdi:wifi-star": 0xfde7,
"mdi:wifi-star": 0xfe6b,
"mdi:wifi-strength-1": 0xf91e,
"mdi:wifi-strength-1-alert": 0xf91f,
"mdi:wifi-strength-1-lock": 0xf920,
@ -3659,7 +3759,9 @@ class MaterialDesignIcons {
"mdi:xbox-controller-battery-low": 0xf74d,
"mdi:xbox-controller-battery-medium": 0xf74e,
"mdi:xbox-controller-battery-unknown": 0xf74f,
"mdi:xbox-controller-menu": 0xfe52,
"mdi:xbox-controller-off": 0xf5bb,
"mdi:xbox-controller-view": 0xfe53,
"mdi:xda": 0xf5bc,
"mdi:xing": 0xf5bd,
"mdi:xing-box": 0xf5be,

View File

@ -14,17 +14,18 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _newHassioDomain = "";
String _hassioPort = "";
String _newHassioPort = "";
String _hassioPassword = "";
String _newHassioPassword = "";
String _socketProtocol = "wss";
String _newSocketProtocol = "wss";
bool _useLovelace = true;
bool _newUseLovelace = true;
String oauthUrl;
@override
void initState() {
super.initState();
_loadSettings();
}
_loadSettings() async {
@ -33,7 +34,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
setState(() {
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
try {
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
@ -44,7 +44,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
}
bool _checkConfigChanged() {
return ((_newHassioPassword != _hassioPassword) ||
return (
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
@ -59,7 +59,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("hassio-domain", _newHassioDomain);
prefs.setString("hassio-port", _newHassioPort);
prefs.setString("hassio-password", _newHassioPassword);
prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
prefs.setBool("use-lovelace", _newUseLovelace);
@ -149,21 +148,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
"Try ports 80 and 443 if default is not working and you don't know why.",
style: TextStyle(color: Colors.grey),
),
new TextField(
decoration: InputDecoration(
labelText: "Access token"
),
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _newHassioPassword,
selection:
new TextSelection.collapsed(offset: _newHassioPassword.length)
)
),
onChanged: (value) {
_newHassioPassword = value;
}
),
Padding(
padding: EdgeInsets.only(top: 20.0),
child: Text(

View File

@ -28,7 +28,7 @@ class Panel {
void handleOpen(BuildContext context) {
if (type == "iframe") {
Logger.d("Launching custom tab with ${config["url"]}");
HAUtils.launchURLInCustomTab(context, config["url"]);
HAUtils.launchURLInCustomTab(context: context, url: config["url"]);
} else if (type == "config") {
Navigator.of(context).push(
MaterialPageRoute(
@ -36,9 +36,9 @@ class Panel {
)
);
} else {
String url = "$homeAssistantWebHost/$urlPath";
String url = "${Connection().httpWebHost}/$urlPath";
Logger.d("Launching custom tab with $url");
HAUtils.launchURLInCustomTab(context, url);
HAUtils.launchURLInCustomTab(context: context, url: url);
}
}

View File

@ -1,8 +1,8 @@
part of '../main.dart';
class Sizes {
static const rightWidgetPadding = 16.0;
static const leftWidgetPadding = 16.0;
static const rightWidgetPadding = 10.0;
static const leftWidgetPadding = 10.0;
static const buttonPadding = 4.0;
static const extendedWidgetHeight = 50.0;
static const iconSize = 28.0;

View File

@ -4,6 +4,8 @@ class HomeAssistantUI {
List<HAView> views;
String title;
bool get isEmpty => views == null || views.isEmpty;
HomeAssistantUI() {
views = [];
}
@ -25,4 +27,8 @@ class HomeAssistantUI {
return result;
}
void clear() {
views.clear();
}
}

View File

@ -112,7 +112,7 @@ class CardWidget extends StatelessWidget {
if (!entity.entity.isHidden) {
body.add(
Padding(
padding: EdgeInsets.fromLTRB(10.0, 4.0, 0.0, 4.0),
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
child: EntityModel(
entityWrapper: entity,
handleTap: true,
@ -122,7 +122,10 @@ class CardWidget extends StatelessWidget {
}
});
return Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
child: Padding(
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding),
child: Column(mainAxisSize: MainAxisSize.min, children: body),
)
);
}

View File

@ -23,6 +23,60 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
void initState() {
super.initState();
_items = <ConfigurationItem>[
ConfigurationItem(
header: 'Home Assistant Cloud',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/cloud/account");
},
)
],
),
)
),
ConfigurationItem(
header: 'Integrations',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/integrations/dashboard");
},
)
],
),
)
),
ConfigurationItem(
header: 'Users',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/users/picker");
},
)
],
),
)
),
ConfigurationItem(
header: 'General',
body: Padding(
@ -31,6 +85,13 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/core");
},
),
Container(height: Sizes.rowPadding,),
Text("Server management", style: TextStyle(fontSize: Sizes.largeFontSize)),
Container(height: Sizes.rowPadding,),
Text("Control your Home Assistant server from HA Client."),
@ -38,27 +99,202 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatServiceButton(
text: "Restart",
serviceName: "restart",
serviceDomain: "homeassistant",
entityId: null,
FlatButton(
child: Text('Restart', style: TextStyle(color: Colors.blue)),
onPressed: () => restart(),
),
FlatServiceButton(
text: "Stop",
serviceName: "stop",
serviceDomain: "homeassistant",
entityId: null,
FlatButton(
child: Text("Stop", style: TextStyle(color: Colors.blue)),
onPressed: () => stop(),
),
],
)
],
),
)
),
ConfigurationItem(
header: 'Persons',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/person");
},
)
],
),
)
),
ConfigurationItem(
header: 'Entity Registry',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/entity_registry");
},
)
],
),
)
),
ConfigurationItem(
header: 'Area Registry',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/area_registry");
},
)
],
),
)
),
ConfigurationItem(
header: 'Automation',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/automation");
},
)
],
),
)
),
ConfigurationItem(
header: 'Script',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/script");
},
)
],
),
)
),
ConfigurationItem(
header: 'Customization',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
onPressed: () {
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/customize");
},
)
],
),
)
),
ConfigurationItem(
header: 'Mobile app',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text("Registration", style: TextStyle(fontSize: Sizes.largeFontSize)),
Container(height: Sizes.rowPadding,),
Text("${HomeAssistant().userName}'s ${Device().model}, ${Device().osName} ${Device().osVersion}"),
Container(height: 6.0,),
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
Divider(),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
onPressed: () => updateRegistration(),
child: Text("Check registration", style: TextStyle(color: Colors.blue))
),
FlatButton(
onPressed: () => resetRegistration(),
child: Text("Reset registration", style: TextStyle(color: Colors.red))
)
],
)
],
),
)
)
];
}
restart() {
eventBus.fire(ShowDialogEvent(
title: "Are you sure you want to restart Home Assistant?",
body: "This will restart your Home Assistant server.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
Connection().callService(domain: "homeassistant", service: "restart", entityId: null);
},
));
}
stop() {
eventBus.fire(ShowDialogEvent(
title: "Are you sure you wanr to STOP Home Assistant?",
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
Connection().callService(domain: "homeassistant", service: "stop", entityId: null);
},
));
}
updateRegistration() {
HomeAssistant().checkAppRegistration(showOkDialog: true);
}
resetRegistration() {
eventBus.fire(ShowDialogEvent(
title: "Waaaait",
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
positiveText: "Done it already",
negativeText: "Ok, I will",
onPositive: () {
HomeAssistant().checkAppRegistration(showOkDialog: true, forceRegister: true);
},
));
}
@override
Widget build(BuildContext context) {

View File

@ -45,6 +45,50 @@ class Logger {
}
class HAError {
String message;
final List<HAErrorAction> actions;
HAError(this.message, {this.actions: const [HAErrorAction.tryAgain()]});
HAError.unableToConnect({this.actions = const [HAErrorAction.tryAgain()]}) {
this.message = "Unable to connect to Home Assistant";
}
HAError.disconnected({this.actions = const [HAErrorAction.reconnect()]}) {
this.message = "Disconnected";
}
HAError.checkConnectionSettings({this.actions = const [HAErrorAction.reload(), HAErrorAction(title: "Settings", type: HAErrorActionType.OPEN_CONNECTION_SETTINGS)]}) {
this.message = "Check connection settings";
}
}
class HAErrorAction {
final String title;
final int type;
final String url;
const HAErrorAction({@required this.title, this.type: HAErrorActionType.FULL_RELOAD, this.url});
const HAErrorAction.tryAgain({this.title = "Try again", this.type = HAErrorActionType.FULL_RELOAD, this.url});
const HAErrorAction.reconnect({this.title = "Reconnect", this.type = HAErrorActionType.FULL_RELOAD, this.url});
const HAErrorAction.reload({this.title = "Reload", this.type = HAErrorActionType.FULL_RELOAD, this.url});
const HAErrorAction.loginAgain({this.title = "Login again", this.type = HAErrorActionType.FULL_RELOAD, this.url});
}
class HAErrorActionType {
static const FULL_RELOAD = 0;
static const QUICK_RELOAD = 1;
static const LOGOUT = 2;
static const URL = 3;
static const OPEN_CONNECTION_SETTINGS = 4;
}
class HAUtils {
static void launchURL(String url) async {
if (await urlLauncher.canLaunch(url)) {
@ -54,15 +98,15 @@ class HAUtils {
}
}
static void launchURLInCustomTab(BuildContext context, String url) async {
static void launchURLInCustomTab({BuildContext context, String url, bool enableDefaultShare: true, bool showPageTitle: true}) async {
try {
await launch(
"$url",
option: new CustomTabsOption(
toolbarColor: Theme.of(context).primaryColor,
enableDefaultShare: true,
enableDefaultShare: enableDefaultShare,
enableUrlBarHiding: true,
showPageTitle: true,
showPageTitle: showPageTitle,
animation: new CustomTabsAnimation.slideIn()
// or user defined animation.
/*animation: new CustomTabsAnimation(
@ -109,6 +153,17 @@ class RefreshDataFinishedEvent {
RefreshDataFinishedEvent();
}
class ReloadUIEvent {
ReloadUIEvent();
}
class StartAuthEvent {
String oauthUrl;
bool showButton;
StartAuthEvent(this.oauthUrl, this.showButton);
}
class ServiceCallEvent {
String domain;
String service;
@ -118,6 +173,17 @@ class ServiceCallEvent {
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
}
class ShowDialogEvent {
final String title;
final String body;
final String positiveText;
final String negativeText;
final onPositive;
final onNegative;
ShowDialogEvent({this.title, this.body, this.positiveText: "Ok", this.negativeText: "Cancel", this.onPositive, this.onNegative});
}
class ShowEntityPageEvent {
Entity entity;
@ -125,8 +191,7 @@ class ShowEntityPageEvent {
}
class ShowErrorEvent {
String text;
int errorCode;
final HAError error;
ShowErrorEvent(this.text, this.errorCode);
ShowErrorEvent(this.error);
}

View File

@ -1,5 +1,5 @@
# Generated by pub
# See https://www.dartlang.org/tools/pub/glossary#lockfile
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
@ -7,21 +7,21 @@ packages:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.0.10"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.1"
version: "1.5.2"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.2.0"
boolean_selector:
dependency: transitive
description:
@ -35,7 +35,7 @@ packages:
name: cached_network_image
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.0"
version: "1.1.1"
charcode:
dependency: transitive
description:
@ -77,16 +77,7 @@ packages:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
dart_config:
dependency: transitive
description:
path: "."
ref: HEAD
resolved-ref: a7ed88a4793e094a4d5d5c2d88a89e55510accde
url: "https://github.com/MarkOSullivan94/dart_config.git"
source: git
version: "0.5.0"
version: "2.1.1+1"
date_format:
dependency: "direct main"
description:
@ -94,13 +85,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.6"
device_info:
dependency: "direct main"
description:
name: device_info
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0+2"
event_bus:
dependency: "direct main"
description:
name: event_bus
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
version: "1.1.0"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0+4"
flutter:
dependency: "direct main"
description: flutter
@ -112,7 +117,7 @@ packages:
name: flutter_cache_manager
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.2"
version: "1.1.1"
flutter_custom_tabs:
dependency: "direct main"
description:
@ -126,7 +131,14 @@ packages:
name: flutter_launcher_icons
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.0"
version: "0.7.2+1"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.1+3"
flutter_markdown:
dependency: "direct main"
description:
@ -134,25 +146,32 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
flutter_svg:
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_svg
name: flutter_secure_storage
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.4"
version: "3.2.1+1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_webview_plugin:
dependency: "direct main"
description:
name: flutter_webview_plugin
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.7"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.0+1"
version: "0.12.0+2"
http_parser:
dependency: transitive
description:
@ -166,14 +185,14 @@ packages:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
version: "2.1.4"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.7"
version: "0.15.8"
logging:
dependency: transitive
description:
@ -187,14 +206,14 @@ packages:
name: markdown
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.0.3"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.3+1"
version: "0.12.5"
meta:
dependency: transitive
description:
@ -209,62 +228,55 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.2"
path_drawing:
dependency: transitive
description:
name: path_drawing
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
path_parsing:
dependency: transitive
description:
name: path_parsing
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
path_provider:
dependency: transitive
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0+1"
version: "1.2.0"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
version: "1.7.0"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
version: "2.4.0"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
progress_indicators:
dependency: "direct main"
description:
name: progress_indicators
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
version: "0.1.4"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
version: "2.0.3"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.1+1"
version: "0.5.3+4"
sky_engine:
dependency: transitive
description: flutter
@ -276,14 +288,14 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.4"
version: "1.5.5"
sqflite:
dependency: transitive
description:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
version: "1.1.6+3"
stack_trace:
dependency: transitive
description:
@ -297,7 +309,7 @@ packages:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.8"
version: "2.0.0"
string_scanner:
dependency: transitive
description:
@ -311,7 +323,7 @@ packages:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.0+1"
term_glyph:
dependency: transitive
description:
@ -325,7 +337,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.2"
version: "0.2.5"
typed_data:
dependency: transitive
description:
@ -339,14 +351,14 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.2"
version: "5.1.2"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.0.2"
vector_math:
dependency: transitive
description:
@ -360,7 +372,7 @@ packages:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.9"
version: "1.0.15"
xml:
dependency: transitive
description:
@ -374,7 +386,7 @@ packages:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.15"
version: "2.1.16"
sdks:
dart: ">=2.1.0 <3.0.0"
flutter: ">=1.2.1 <2.0.0"
dart: ">=2.4.0 <3.0.0"
flutter: ">=1.5.0 <2.0.0"

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 0.5.0+97
version: 0.6.0+603
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -18,8 +18,13 @@ dependencies:
date_format: any
charts_flutter: any
flutter_markdown: any
flutter_svg: ^0.10.3
# flutter_svg: ^0.10.3
flutter_custom_tabs: ^0.6.0
firebase_messaging: ^4.0.0+1
flutter_webview_plugin: ^0.3.1
flutter_secure_storage: ^3.2.0
device_info: ^0.4.0+1
flutter_local_notifications: ^0.7.1+3
dev_dependencies:
flutter_test:
@ -60,7 +65,7 @@ flutter:
fonts:
- family: "Material Design Icons"
fonts:
- asset: fonts/materialdesignicons-webfont-3-5-95.ttf
- asset: fonts/materialdesignicons-webfont-3-6-95.ttf
# fonts:
# - family: Schyler
# fonts:

File diff suppressed because one or more lines are too long