Compare commits

...

56 Commits

Author SHA1 Message Date
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
db77cc43aa Version 0.5.0 2019-03-13 22:42:03 +02:00
b2269cc96d Resolves #293 Fix updater icon 2019-03-13 22:40:54 +02:00
8b28bb2e9e Resolves #314 card icon priority 2019-03-13 22:12:01 +02:00
fb456878bc Resolves #258 Timer support 2019-03-13 21:33:58 +02:00
8b961ebd69 Resolves #83 Calendar support 2019-03-13 20:07:44 +02:00
9bd3a41cf5 Resolves #140 Scenes 2019-03-13 18:06:43 +02:00
491ae55a2a Resolves #299, Resolves #234 Fix entity picture url issue 2019-03-13 17:48:49 +02:00
e1d2981782 Add 'Open Web UI' menu link 2019-03-13 17:25:08 +02:00
74572168ae Resolves #116 Add Iframe panel support 2019-03-13 17:23:23 +02:00
92d0b5c055 Migrate to AndroidX 2019-03-13 17:05:15 +02:00
3504d3276c Resolves #11 Add Panels fetching 2019-03-13 16:39:23 +02:00
736b38b64c Some UI improvements for #245 2019-03-13 14:08:54 +02:00
cb118b599a Resolves #245 Add special row elements support for entities card 2019-03-13 00:56:57 +02:00
a08a056cff Resolves #254 Missed entities 2019-03-12 23:35:33 +02:00
0ef2ebfe31 Fix 'Paste color' button background when saved color is null 2019-03-10 23:49:05 +02:00
4f4ac3b574 Resolves #310 Add assumed state for locks 2019-03-10 23:41:14 +02:00
7064cb0e30 Resolves #272 Add 'Copy color' and 'Past color' 2019-03-10 23:28:23 +02:00
91a99e17e0 Resolves #320 Fix eEntity_picture size 2019-03-10 22:50:39 +02:00
2e9b7d20b9 Fix broken icons 2019-03-10 19:28:11 +02:00
b8aa808de4 Update Material Design Icons to 3.5.95 2019-03-09 13:26:45 +00:00
2cfa92a42b Reverts #308 2019-03-06 16:50:30 +00:00
146efef72d Gradle config for Chrome OS build 2019-03-06 16:42:05 +00:00
8c9804e16f WIP #308 2019-03-02 20:13:24 +02:00
a4736bfb5a Message handling improvements 2019-03-02 18:00:25 +02:00
15c54df629 Update README.md 2019-02-26 11:31:39 +02:00
32ffef21e9 Update README.md 2019-02-26 11:31:08 +02:00
848d3cb510 Update README.md 2019-02-26 10:45:25 +02:00
8a4caeebba Update README.md 2019-02-26 10:43:47 +02:00
aa923f0fba Update README.md 2019-02-26 10:39:09 +02:00
4d8f50ddd5 Update README.md 2019-02-26 10:33:34 +02:00
fe06b21a6c Update README.md 2019-02-26 10:30:08 +02:00
efed7fb1b5 Update README.md 2019-02-26 10:23:03 +02:00
df2cbb7d13 Resolves #313 Fix missed mute button for media_player 2019-02-22 15:39:53 +02:00
03edaa9ca2 Resolves #168 Fix error when entity view closed before history loaded 2019-02-22 15:33:10 +02:00
1a7457abf9 Resolves #311 Rebuild tabs only if views count changes 2019-02-22 15:28:11 +02:00
00889b13e0 Resolves #312 Add white value control for light 2019-02-22 15:15:27 +02:00
0615073ec4 Get color from rgb_color if there is no hsv_color attribute 2019-02-22 14:20:01 +02:00
eb7d17d147 WIP #308 Move entity icon generation into EntityIcon widget 2019-02-21 16:32:55 +02:00
24f80feeee Resolves #187 Fix crash on view count changes 2019-02-21 15:35:58 +02:00
73 changed files with 2594 additions and 872 deletions

View File

@ -1,13 +1,12 @@
[![flutter](https://somegeeky.website/assets/badges/flutter_badge_v3.svg)](https://somegeeky.website/badges/flutter) [![dart](https://somegeeky.website/assets/badges/dart_badge_v3.svg)](https://somegeeky.website/badges/dart)
# HA Client # HA Client
## Native Android client for Home Assistant ## Native Android client for Home Assistant
### With Lovelace UI support ### With Lovelace UI support
Home Assistant Android client on Dart with Flutter. Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
Visit [www.vynn.co](https://www.vynn.co/ha-client) for more info.
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or in [Discord](https://discord.gg/NSaQEQ8) Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912)

View File

@ -29,7 +29,7 @@ def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android { android {
compileSdkVersion 27 compileSdkVersion 28
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -43,7 +43,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.keyboardcrumbs.haclient" applicationId "com.keyboardcrumbs.haclient"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 27 targetSdkVersion 28
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@ -70,7 +70,10 @@ flutter {
} }
dependencies { dependencies {
implementation 'com.google.firebase:firebase-core:16.0.8'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
} }
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

@ -15,7 +15,8 @@
<application <application
android:name="io.flutter.app.FlutterApplication" android:name="io.flutter.app.FlutterApplication"
android:label="HA Client" android:label="HA Client"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
@ -30,6 +31,10 @@
<meta-data <meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame" 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> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>

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 { 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

@ -1 +1,5 @@
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx2g
org.gradle.daemon=true
org.gradle.caching=true
android.useAndroidX=true
android.enableJetifier=true

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

0
android/gradlew vendored Normal file → Executable file
View File

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,44 @@
part of 'main.dart';
class AuthManager {
static final AuthManager _instance = AuthManager._internal();
factory AuthManager() {
return _instance;
}
AuthManager._internal();
Future getTempToken({String httpWebHost, String oauthUrl}) {
Completer completer = Completer();
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.onUrlChanged.listen((String 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(
host: httpWebHost,
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();
completer.complete(tempToken);
}).catchError((e) {
flutterWebviewPlugin.close();
completer.completeError({"errorCode": 61, "errorMessage": "Error getting temp token"});
Logger.e("Error getting temp token: ${e.toString()}");
});
}
});
Logger.d("Launching OAuth...");
eventBus.fire(StartAuthEvent(oauthUrl));
return completer.future;
}
}

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

@ -0,0 +1,343 @@
part of 'main.dart';
class Connection {
static final Connection _instance = Connection._internal();
factory Connection() {
return _instance;
}
Connection._internal();
String displayHostname;
String _webSocketAPIEndpoint;
String httpWebHost;
String _token;
String _tempToken;
String oauthUrl;
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(onStateChange) async {
Completer completer = Completer();
onStateChangeCallback = onStateChange;
SharedPreferences prefs = await SharedPreferences.getInstance();
String domain = prefs.getString('hassio-domain');
String port = prefs.getString('hassio-port');
displayHostname = "$domain:$port";
_webSocketAPIEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
httpWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
//_token = prefs.getString('hassio-token');
final storage = new FlutterSecureStorage();
try {
_token = await storage.read(key: "hacl_llt");
} catch (e) {
Logger.e("Cannt read secure storage. Need to relogin.");
_token = null;
await storage.delete(key: "hacl_llt");
}
if ((domain == null) || (port == null) ||
(domain.length == 0) || (port.length == 0)) {
completer.completeError({"errorCode": 5, "errorMessage": "Check connection settings"});
} else {
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')}";
if (_token == null) {
await AuthManager().getTempToken(
httpWebHost: httpWebHost,
oauthUrl: oauthUrl
).then((token) {
Logger.d("Token from AuthManager recived");
_tempToken = token;
});
}
_connect().timeout(connectTimeout, onTimeout: () {
_disconnect().then((_) {
completer.completeError(
{"errorCode": 1, "errorMessage": "Connection timeout"});
});
}).then((_) => completer.complete()).catchError((e) {
completer.completeError(e);
});
}
return completer.future;
}
Completer connecting;
Future _connect() async {
if (connecting != null && !connecting.isCompleted) {
Logger.w("");
return connecting.future;
}
connecting = Completer();
await _disconnect();
Logger.d( "Socket connecting...");
_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((_) => 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 (!connecting.isCompleted) connecting.complete(sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "state_changed"},
));
} else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.completeError({"errorCode": 62, "errorMessage": "${data["message"]}"});
_messageResolver.remove("auth");
logout().then((_) {
if (!connecting.isCompleted) connecting.completeError({"errorCode": 62, "errorMessage": "${data["message"]}"});
});
} else {
_handleMessage(data);
}
},
cancelOnError: true,
onDone: () => _handleSocketClose(connecting),
onError: (e) => _handleSocketError(e, connecting)
);
return connecting.future;
}
Future _disconnect() async {
Logger.d( "Socket disconnecting...");
await _socketSubscription?.cancel();
await _socket?.sink?.close()?.timeout(Duration(seconds: 4),
onTimeout: () => Logger.d( "Socket sink close timeout")
);
Logger.d( "..Disconnected");
}
_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({"errorMessage": "${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) {
isConnected = false;
Logger.d("Socket disconnected.");
if (!connectionCompleter.isCompleted) {
connectionCompleter.completeError({"errorCode": 82, "errorMessage": "Disconnected"});
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect();
});
});
}
}
void _handleSocketError(e, Completer connectionCompleter) {
isConnected = false;
Logger.e("Socket stream Error: $e");
if (!connectionCompleter.isCompleted) {
connectionCompleter.completeError({"errorCode": 81, "errorMessage": "Unable to connect to Home Assistant"});
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect();
});
});
}
}
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((_) {
completer.complete();
}).catchError((e) {
Logger.e("Can't get long-lived token: $e");
throw e;
});
}).catchError((e) => completer.completeError(e));
} else {
completer.completeError({"errorCode": 63, "errorMessage": "General login error"});
}
return completer.future;
}
Future logout() {
_token = null;
_tempToken = null;
final storage = new FlutterSecureStorage();
return storage.delete(key: "hacl_llt");
}
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({"errorCode": 63, "errorMessage": "Authentication error: $e"});
});
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);
Logger.d("[Sending] ==> $rawMessage");
if (!isConnected) {
_connect().timeout(connectTimeout, onTimeout: (){
_completer.completeError({"errorCode": 8, "errorMessage": "No connection to Home Assistant"});
}).then((_) {
_socket.sink.add(rawMessage);
}).catchError((e) {
_completer.completeError(e);
});
} else {
_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 host, String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true, String authToken}) async {
Completer completer = Completer();
String url = "$host$endPoint";
Logger.d("[Sending] ==> $url");
Map<String, String> headers = {};
if (contentType != null) {
headers["Content-Type"] = contentType;
}
if (includeAuthHeader) {
headers["authorization"] = "Bearer $authToken";
}
http.post(
url,
headers: headers,
body: data
).then((response) {
Logger.d("[Received] <== ${response.statusCode}, ${response.body}");
if (response.statusCode == 200) {
completer.complete(response.body);
} else {
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
}
}).catchError((e) {
completer.completeError(e);
});
return completer.future;
}
}

View File

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

View File

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

View File

@ -1,7 +1,8 @@
part of '../main.dart'; part of '../main.dart';
class ButtonEntity extends Entity { class ButtonEntity extends Entity {
ButtonEntity(Map rawData) : super(rawData); ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);
@override @override
Widget _buildStatePart(BuildContext context) { Widget _buildStatePart(BuildContext context) {
@ -9,7 +10,7 @@ class ButtonEntity extends Entity {
entityId: entityId, entityId: entityId,
serviceDomain: domain, serviceDomain: domain,
serviceName: 'turn_on', serviceName: 'turn_on',
text: "EXECUTE", text: domain == "scene" ? "ACTIVATE" : "EXECUTE",
); );
} }
} }

View File

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

View File

@ -23,6 +23,8 @@ class ClimateEntity extends Entity {
static const SUPPORT_AUX_HEAT = 2048; static const SUPPORT_AUX_HEAT = 2048;
static const SUPPORT_ON_OFF = 4096; static const SUPPORT_ON_OFF = 4096;
ClimateEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportTargetTemperature => ((supportedFeatures & bool get supportTargetTemperature => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) == ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE); ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
@ -88,11 +90,9 @@ class ClimateEntity extends Entity {
bool get isOff => state == EntityState.off; bool get isOff => state == EntityState.off;
bool get auxHeat => attributes['aux_heat'] == "on"; bool get auxHeat => attributes['aux_heat'] == "on";
ClimateEntity(Map rawData) : super(rawData);
@override @override
void update(Map rawData) { void update(Map rawData, String webHost) {
super.update(rawData); super.update(rawData, webHost);
if (supportTargetTemperature) { if (supportTargetTemperature) {
historyConfig.numericAttributesToShow.add("temperature"); historyConfig.numericAttributesToShow.add("temperature");
} }

View File

@ -28,6 +28,7 @@ class EntityState {
static const unavailable = 'unavailable'; static const unavailable = 'unavailable';
static const ok = 'ok'; static const ok = 'ok';
static const problem = 'problem'; static const problem = 'problem';
static const active = 'active';
} }
class EntityUIAction { class EntityUIAction {

View File

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

View File

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

View File

@ -1,5 +1,14 @@
part of '../main.dart'; part of '../main.dart';
class StatelessEntityType {
static const NONE = 0;
static const MISSED = 1;
static const DIVIDER = 2;
static const SECTION = 3;
static const CALL_SERVICE = 4;
static const WEBLINK = 5;
}
class Entity { class Entity {
static List badgeDomains = [ static List badgeDomains = [
@ -64,12 +73,13 @@ class Entity {
Map attributes; Map attributes;
String domain; String domain;
String entityId; String entityId;
String entityPicture;
String state; String state;
String displayState; String displayState;
DateTime _lastUpdated; DateTime _lastUpdated;
int statelessType = 0;
List<Entity> childEntities = []; List<Entity> childEntities = [];
List<String> attributesToShow = ["all"];
String deviceClass; String deviceClass;
EntityHistoryConfig historyConfig = EntityHistoryConfig( EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.simple chartType: EntityHistoryWidgetType.simple
@ -85,7 +95,6 @@ class Entity {
bool get isBadge => Entity.badgeDomains.contains(domain); bool get isBadge => Entity.badgeDomains.contains(domain);
String get icon => attributes["icon"] ?? ""; String get icon => attributes["icon"] ?? "";
bool get isOn => state == EntityState.on; bool get isOn => state == EntityState.on;
String get entityPicture => attributes["entity_picture"];
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? ""; String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
List get childEntityIds => attributes["entity_id"] ?? []; List get childEntityIds => attributes["entity_id"] ?? [];
String get lastUpdated => _getLastUpdatedFormatted(); String get lastUpdated => _getLastUpdatedFormatted();
@ -93,11 +102,53 @@ class Entity {
double get doubleState => double.tryParse(state) ?? 0.0; double get doubleState => double.tryParse(state) ?? 0.0;
int get supportedFeatures => attributes["supported_features"] ?? 0; int get supportedFeatures => attributes["supported_features"] ?? 0;
Entity(Map rawData) { String _getEntityPictureUrl(String webHost) {
update(rawData); String result = attributes["entity_picture"];
if (result == null) return result;
if (!result.startsWith("http")) {
if (result.startsWith("/")) {
result = "$webHost$result";
} else {
result = "$webHost/$result";
}
}
return result;
} }
void update(Map rawData) { Entity(Map rawData, String webHost) {
update(rawData, webHost);
}
Entity.missed(String entityId) {
statelessType = StatelessEntityType.MISSED;
attributes = {"hidden": false};
this.entityId = entityId;
}
Entity.divider() {
statelessType = StatelessEntityType.DIVIDER;
attributes = {"hidden": false};
}
Entity.section(String label) {
statelessType = StatelessEntityType.SECTION;
attributes = {"hidden": false, "friendly_name": "$label"};
}
Entity.callService({String icon, String name, String service, String actionName}) {
statelessType = StatelessEntityType.CALL_SERVICE;
entityId = service;
displayState = actionName?.toUpperCase() ?? "RUN";
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
}
Entity.weblink({String url, String name, String icon}) {
statelessType = StatelessEntityType.WEBLINK;
entityId = "custom.custom"; //TODO wtf??
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
}
void update(Map rawData, String webHost) {
attributes = rawData["attributes"] ?? {}; attributes = rawData["attributes"] ?? {};
domain = rawData["entity_id"].split(".")[0]; domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"]; entityId = rawData["entity_id"];
@ -105,6 +156,7 @@ class Entity {
state = rawData["state"]; state = rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state; displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
_lastUpdated = DateTime.tryParse(rawData["last_updated"]); _lastUpdated = DateTime.tryParse(rawData["last_updated"]);
entityPicture = _getEntityPictureUrl(webHost);
} }
double _getDoubleAttributeValue(String attributeName) { double _getDoubleAttributeValue(String attributeName) {

View File

@ -4,6 +4,7 @@ class EntityWrapper {
String displayName; String displayName;
String icon; String icon;
String entityPicture;
EntityUIAction uiAction; EntityUIAction uiAction;
Entity entity; Entity entity;
@ -14,10 +15,15 @@ class EntityWrapper {
String displayName, String displayName,
this.uiAction this.uiAction
}) { }) {
this.icon = icon ?? entity.icon; if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
this.displayName = displayName ?? entity.displayName; this.icon = icon ?? entity.icon;
if (this.uiAction == null) { if (icon == null) {
this.uiAction = EntityUIAction(); entityPicture = entity.entityPicture;
}
this.displayName = displayName ?? entity.displayName;
if (uiAction == null) {
uiAction = EntityUIAction();
}
} }
} }
@ -49,6 +55,16 @@ class EntityWrapper {
break; break;
} }
case EntityUIAction.navigate: {
if (uiAction.tapService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
HAUtils.launchURL(uiAction.tapService);
}
break;
}
default: { default: {
break; break;
} }
@ -79,6 +95,16 @@ class EntityWrapper {
break; break;
} }
case EntityUIAction.navigate: {
if (uiAction.holdService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
HAUtils.launchURL(uiAction.holdService);
}
break;
}
default: { default: {
break; break;
} }

View File

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

View File

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

View File

@ -33,6 +33,7 @@ class LightEntity extends Entity {
LightEntity.SUPPORT_WHITE_VALUE); LightEntity.SUPPORT_WHITE_VALUE);
int get brightness => _getIntAttributeValue("brightness"); int get brightness => _getIntAttributeValue("brightness");
int get whiteValue => _getIntAttributeValue("white_value");
String get effect => attributes["effect"]; String get effect => attributes["effect"];
int get colorTemp => _getIntAttributeValue("color_temp"); int get colorTemp => _getIntAttributeValue("color_temp");
double get maxMireds => _getDoubleAttributeValue("max_mireds"); double get maxMireds => _getDoubleAttributeValue("max_mireds");
@ -41,15 +42,18 @@ class LightEntity extends Entity {
bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0)); bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0));
List<String> get effectList => getStringListAttributeValue("effect_list"); List<String> get effectList => getStringListAttributeValue("effect_list");
LightEntity(Map rawData) : super(rawData); LightEntity(Map rawData, String webHost) : super(rawData, webHost);
HSVColor _getColor() { HSVColor _getColor() {
List hs = attributes["hs_color"]; List hs = attributes["hs_color"];
List rgb = attributes["rgb_color"];
try { try {
if ((hs != null) && (hs.length > 0)) { if (hs != null && hs.isNotEmpty) {
double sat = hs[1]/100; double sat = hs[1]/100;
String ssat = sat.toStringAsFixed(2); String ssat = sat.toStringAsFixed(2);
return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0); return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0);
} else if (rgb != null && rgb.isNotEmpty) {
return HSVColor.fromColor(Color.fromARGB(255, rgb[0], rgb[1], rgb[2]));
} else { } else {
return null; return null;
} }

View File

@ -1,12 +1,21 @@
part of '../main.dart'; part of '../main.dart';
class LockEntity extends Entity { class LockEntity extends Entity {
LockEntity(Map rawData) : super(rawData); LockEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get isLocked => state == "locked"; bool get isLocked => state == "locked";
@override @override
Widget _buildStatePart(BuildContext context) { Widget _buildStatePart(BuildContext context) {
return LockStateWidget(); return LockStateWidget(
assumedState: false,
);
}
@override
Widget _buildStatePartForPage(BuildContext context) {
return LockStateWidget(
assumedState: true,
);
} }
} }

View File

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

View File

@ -1,7 +1,7 @@
part of '../main.dart'; part of '../main.dart';
class SunEntity extends Entity { class SunEntity extends Entity {
SunEntity(Map rawData) : super(rawData); SunEntity(Map rawData, String webHost) : super(rawData, webHost);
} }
class SensorEntity extends Entity { class SensorEntity extends Entity {
@ -12,6 +12,6 @@ class SensorEntity extends Entity {
numericState: true 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>() ? (attributes["options"] as List).cast<String>()
: []; : [];
SelectEntity(Map rawData) : super(rawData); SelectEntity(Map rawData, String webHost) : super(rawData, webHost);
@override @override
Widget _buildStatePart(BuildContext context) { Widget _buildStatePart(BuildContext context) {

View File

@ -1,7 +1,7 @@
part of '../main.dart'; part of '../main.dart';
class SliderEntity extends Entity { 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 minValue => _getDoubleAttributeValue("min") ?? 0.0;
double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0; double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0;

View File

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

View File

@ -1,7 +1,7 @@
part of '../main.dart'; part of '../main.dart';
class TextEntity extends Entity { 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 valueMinLength => attributes["min"] ?? -1;
int get valueMaxLength => attributes["max"] ?? -1; int get valueMaxLength => attributes["max"] ?? -1;

View File

@ -0,0 +1,45 @@
part of '../main.dart';
class TimerEntity extends Entity {
TimerEntity(Map rawData, String webHost) : super(rawData, webHost);
Duration duration;
@override
void update(Map rawData, String webHost) {
super.update(rawData, webHost);
String durationSource = "${attributes["duration"]}";
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 {
duration = Duration(seconds: 0);
}
}
@override
Widget _buildStatePart(BuildContext context) {
return TimerState();
}
}

View File

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

View File

@ -9,7 +9,12 @@ class ButtonEntityContainer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
return InkWell( return InkWell(
onTap: () => entityWrapper.handleTap(), onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(), onLongPress: () => entityWrapper.handleHold(),
@ -19,11 +24,11 @@ class ButtonEntityContainer extends StatelessWidget {
FractionallySizedBox( FractionallySizedBox(
widthFactor: 0.4, widthFactor: 0.4,
child: FittedBox( child: FittedBox(
fit: BoxFit.fitHeight, fit: BoxFit.fitHeight,
child: EntityIcon( child: EntityIcon(
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0), padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
iconSize: Sizes.iconSize, size: Sizes.iconSize,
) )
), ),
), ),
_buildName() _buildName()

View File

@ -14,11 +14,11 @@ class BadgeWidget extends StatelessWidget {
{ {
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon" badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
? Icon( ? Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc), MaterialDesignIcons.getIconDataFromIconCode(0xf0dc),
size: iconSize, size: iconSize,
) )
: Icon( : Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8), MaterialDesignIcons.getIconDataFromIconCode(0xf5a8),
size: iconSize, size: iconSize,
); );
break; break;
@ -27,14 +27,20 @@ class BadgeWidget extends StatelessWidget {
case "media_player": case "media_player":
case "binary_sensor": case "binary_sensor":
{ {
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData( badgeIcon = EntityIcon(
entityModel.entityWrapper, iconSize, Colors.black); padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
);
break; break;
} }
case "device_tracker": case "device_tracker":
{ {
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData( badgeIcon = EntityIcon(
entityModel.entityWrapper, iconSize, Colors.black); padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
);
onBadgeTextValue = entityModel.entityWrapper.entity.state; onBadgeTextValue = entityModel.entityWrapper.entity.state;
break; break;
} }

View File

@ -16,6 +16,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
} }
CameraEntity _entity; CameraEntity _entity;
String _webHost;
http.Client client; http.Client client;
http.StreamedResponse response; http.StreamedResponse response;
@ -28,7 +29,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
void _connect() async { void _connect() async {
started = true; started = true;
timeToStop = false; timeToStop = false;
String streamUrl = '$homeAssistantWebHost/api/camera_proxy_stream/${_entity.entityId}?token=${_entity.attributes['access_token']}'; String streamUrl = '$_webHost/api/camera_proxy_stream/${_entity.entityId}?token=${_entity.attributes['access_token']}';
client = new http.Client(); // create a client to make api calls client = new http.Client(); // create a client to make api calls
http.Request request = new http.Request("GET", Uri.parse(streamUrl)); // create get request http.Request request = new http.Request("GET", Uri.parse(streamUrl)); // create get request
Logger.d("[Sending] ==> $streamUrl"); Logger.d("[Sending] ==> $streamUrl");
@ -130,6 +131,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
.of(context) .of(context)
.entityWrapper .entityWrapper
.entity; .entity;
_webHost = HomeAssistantModel.of(context).homeAssistant.connection.httpWebHost;
_connect(); _connect();
} }

View File

@ -7,20 +7,9 @@ class EntityAttributesList extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final entityModel = EntityModel.of(context); final entityModel = EntityModel.of(context);
List<Widget> attrs = []; List<Widget> attrs = [];
if ((entityModel.entityWrapper.entity.attributesToShow == null) || entityModel.entityWrapper.entity.attributes.forEach((name, value) {
(entityModel.entityWrapper.entity.attributesToShow.contains("all"))) { attrs.add(_buildSingleAttribute("$name", "${value ?? '-'}"));
entityModel.entityWrapper.entity.attributes.forEach((name, value) { });
attrs.add(_buildSingleAttribute("$name", "$value"));
});
} else {
entityModel.entityWrapper.entity.attributesToShow.forEach((String attr) {
String attrValue = entityModel.entityWrapper.entity.getAttribute("$attr");
if (attrValue != null) {
attrs.add(
_buildSingleAttribute("$attr", "$attrValue"));
}
});
}
return Padding( return Padding(
padding: EdgeInsets.only(bottom: Sizes.rowPadding), padding: EdgeInsets.only(bottom: Sizes.rowPadding),
child: Column( child: Column(
@ -49,7 +38,7 @@ class EntityAttributesList extends StatelessWidget {
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), 0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
child: Text( child: Text(
"$value", "${value}",
textAlign: TextAlign.right, textAlign: TextAlign.right,
), ),
), ),

View File

@ -440,13 +440,13 @@ class TemperatureControlWidget extends StatelessWidget {
Column( Column(
children: <Widget>[ children: <Widget>[
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
'mdi:chevron-up')), 'mdi:chevron-up')),
iconSize: 30.0, iconSize: 30.0,
onPressed: () => onInc(), onPressed: () => onInc(),
), ),
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
'mdi:chevron-down')), 'mdi:chevron-down')),
iconSize: 30.0, iconSize: 30.0,
onPressed: () => onDec(), onPressed: () => onDec(),

View File

@ -157,7 +157,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
if (entity.supportOpenTilt) { if (entity.supportOpenTilt) {
buttons.add(IconButton( buttons.add(IconButton(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName( MaterialDesignIcons.getIconDataFromIconName(
"mdi:arrow-top-right"), "mdi:arrow-top-right"),
size: Sizes.iconSize, size: Sizes.iconSize,
), ),
@ -170,7 +170,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
if (entity.supportStopTilt) { if (entity.supportStopTilt) {
buttons.add(IconButton( buttons.add(IconButton(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"), MaterialDesignIcons.getIconDataFromIconName("mdi:stop"),
size: Sizes.iconSize, size: Sizes.iconSize,
), ),
onPressed: () => _stop(entity))); onPressed: () => _stop(entity)));
@ -182,7 +182,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
if (entity.supportCloseTilt) { if (entity.supportCloseTilt) {
buttons.add(IconButton( buttons.add(IconButton(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName( MaterialDesignIcons.getIconDataFromIconName(
"mdi:arrow-bottom-left"), "mdi:arrow-bottom-left"),
size: Sizes.iconSize, size: Sizes.iconSize,
), ),

View File

@ -10,6 +10,7 @@ class LightControlsWidget extends StatefulWidget {
class _LightControlsWidgetState extends State<LightControlsWidget> { class _LightControlsWidgetState extends State<LightControlsWidget> {
int _tmpBrightness; int _tmpBrightness;
int _tmpWhiteValue;
int _tmpColorTemp = 0; int _tmpColorTemp = 0;
HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0); HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0);
bool _changedHere = false; bool _changedHere = false;
@ -17,6 +18,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
void _resetState(LightEntity entity) { void _resetState(LightEntity entity) {
_tmpBrightness = entity.brightness ?? 0; _tmpBrightness = entity.brightness ?? 0;
_tmpWhiteValue = entity.whiteValue ?? 0;
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt(); _tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
_tmpColor = entity.color ?? _tmpColor; _tmpColor = entity.color ?? _tmpColor;
_tmpEffect = entity.effect; _tmpEffect = entity.effect;
@ -38,6 +40,17 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
}); });
} }
void _setWhiteValue(LightEntity entity, double value) {
setState(() {
_tmpWhiteValue = value.round();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"white_value": _tmpWhiteValue}));
});
}
void _setColorTemp(LightEntity entity, double value) { void _setColorTemp(LightEntity entity, double value) {
setState(() { setState(() {
_tmpColorTemp = value.round(); _tmpColorTemp = value.round();
@ -84,6 +97,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
_buildBrightnessControl(entity), _buildBrightnessControl(entity),
_buildWhiteValueControl(entity),
_buildColorTempControl(entity), _buildColorTempControl(entity),
_buildColorControl(entity), _buildColorControl(entity),
_buildEffectControl(entity) _buildEffectControl(entity)
@ -112,6 +126,27 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
} }
} }
Widget _buildWhiteValueControl(LightEntity entity) {
if ((entity.supportWhiteValue) && (_tmpWhiteValue != null)) {
return UniversalSlider(
onChanged: (value) {
setState(() {
_changedHere = true;
_tmpWhiteValue = value.round();
});
},
min: 0.0,
max: 255.0,
onChangeEnd: (value) => _setWhiteValue(entity, value),
value: _tmpWhiteValue == null ? 0.0 : _tmpWhiteValue.toDouble(),
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")),
title: "White",
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildColorTempControl(LightEntity entity) { Widget _buildColorTempControl(LightEntity entity) {
if (entity.supportColorTemp) { if (entity.supportColorTemp) {
return UniversalSlider( return UniversalSlider(
@ -136,30 +171,45 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
Widget _buildColorControl(LightEntity entity) { Widget _buildColorControl(LightEntity entity) {
if (entity.supportColor) { if (entity.supportColor) {
return LightColorPicker( HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor;
color: _tmpColor, return Column(
onColorSelected: (color) => _setColor(entity, color), mainAxisSize: MainAxisSize.min,
children: <Widget>[
LightColorPicker(
color: _tmpColor,
onColorSelected: (color) => _setColor(entity, color),
),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
color: _tmpColor.toColor(),
child: Text('Copy color'),
onPressed: _tmpColor == null ? null : () {
setState(() {
HomeAssistantModel
.of(context)
.homeAssistant
.savedColor = _tmpColor;
});
},
),
FlatButton(
color: savedColor?.toColor() ?? Colors.transparent,
child: Text('Paste color'),
onPressed: savedColor == null ? null : () {
_setColor(entity, savedColor);
},
)
],
)
],
); );
} else { } else {
return Container(width: 0.0, height: 0.0); return Container(width: 0.0, height: 0.0);
} }
} }
void _showColorPicker(LightEntity entity) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
titlePadding: EdgeInsets.all(0.0),
contentPadding: EdgeInsets.all(0.0),
content: LightColorPicker(
color: _tmpColor,
),
);
},
);
}
Widget _buildEffectControl(LightEntity entity) { Widget _buildEffectControl(LightEntity entity) {
if ((entity.supportEffect) && (entity.effectList != null)) { if ((entity.supportEffect) && (entity.effectList != null)) {
return ModeSelectorWidget( return ModeSelectorWidget(

View File

@ -73,7 +73,7 @@ class MediaPlayerWidget extends StatelessWidget {
Widget _buildImage(MediaPlayerEntity entity) { Widget _buildImage(MediaPlayerEntity entity) {
String state = entity.state; 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( return Container(
color: Colors.black, color: Colors.black,
child: Row( child: Row(
@ -81,7 +81,7 @@ class MediaPlayerWidget extends StatelessWidget {
children: <Widget>[ children: <Widget>[
Flexible( Flexible(
child: Image( child: Image(
image: CachedNetworkImageProvider("$homeAssistantWebHost${entity.entityPicture}"), image: CachedNetworkImageProvider("${entity.entityPicture}"),
height: 240.0, height: 240.0,
//width: 320.0, //width: 320.0,
fit: BoxFit.contain, fit: BoxFit.contain,
@ -95,7 +95,7 @@ class MediaPlayerWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Icon( Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:movie"), MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
size: 150.0, size: 150.0,
color: EntityColor.stateColor("$state"), color: EntityColor.stateColor("$state"),
) )
@ -227,7 +227,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
if (showMenu) { if (showMenu) {
result.add( result.add(
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")), "mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity)) onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity))
) )
@ -307,11 +307,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) { if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
Widget muteWidget; Widget muteWidget;
Widget volumeStepWidget; Widget volumeStepWidget;
if (entity.supportVolumeMute) { if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
bool isMuted = entity.attributes["is_volume_muted"] ?? false; bool isMuted = entity.attributes["is_volume_muted"] ?? false;
muteWidget = muteWidget =
IconButton( IconButton(
icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up), icon: Icon(isMuted ? Icons.volume_up : Icons.volume_off),
onPressed: () => _setVolumeMute(!isMuted, entity.entityId) onPressed: () => _setVolumeMute(!isMuted, entity.entityId)
); );
} else { } else {
@ -322,11 +322,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:plus")), icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
onPressed: () => _setVolumeUp(entity.entityId) onPressed: () => _setVolumeUp(entity.entityId)
), ),
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:minus")), icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
onPressed: () => _setVolumeDown(entity.entityId) onPressed: () => _setVolumeDown(entity.entityId)
) )
], ],

View File

@ -11,6 +11,25 @@ class DefaultEntityContainer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context); final EntityModel entityModel = EntityModel.of(context);
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
return Divider();
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Divider(),
Text(
"${entityModel.entityWrapper.entity.displayName}",
style: TextStyle(color: Colors.blue),
)
],
);
}
return InkWell( return InkWell(
onLongPress: () { onLongPress: () {
if (entityModel.handleTap) { if (entityModel.handleTap) {
@ -30,7 +49,9 @@ class DefaultEntityContainer extends StatelessWidget {
Flexible( Flexible(
fit: FlexFit.tight, fit: FlexFit.tight,
flex: 3, flex: 3,
child: EntityName(), child: EntityName(
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
),
), ),
state state
], ],

View File

@ -2,6 +2,8 @@ part of '../main.dart';
class EntityColor { class EntityColor {
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
static const badgeColors = { static const badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0), "default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0) "binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
@ -10,15 +12,16 @@ class EntityColor {
static const _stateColors = { static const _stateColors = {
EntityState.on: Colors.amber, EntityState.on: Colors.amber,
"auto": Colors.amber, "auto": Colors.amber,
EntityState.idle: Colors.amber, EntityState.active: Colors.amber,
EntityState.playing: Colors.amber, EntityState.playing: Colors.amber,
"above_horizon": Colors.amber, "above_horizon": Colors.amber,
EntityState.home: Colors.amber, EntityState.home: Colors.amber,
EntityState.open: Colors.amber, EntityState.open: Colors.amber,
EntityState.off: Color.fromRGBO(68, 115, 158, 1.0), EntityState.off: defaultStateColor,
EntityState.closed: Color.fromRGBO(68, 115, 158, 1.0), EntityState.closed: defaultStateColor,
"below_horizon": Color.fromRGBO(68, 115, 158, 1.0), "below_horizon": defaultStateColor,
"default": Color.fromRGBO(68, 115, 158, 1.0), "default": defaultStateColor,
EntityState.idle: defaultStateColor,
"heat": Colors.redAccent, "heat": Colors.redAccent,
"cool": Colors.lightBlue, "cool": Colors.lightBlue,
EntityState.unavailable: Colors.black26, EntityState.unavailable: Colors.black26,

View File

@ -3,20 +3,71 @@ part of '../main.dart';
class EntityIcon extends StatelessWidget { class EntityIcon extends StatelessWidget {
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
final double iconSize; final double size;
final Color color;
const EntityIcon({Key key, this.iconSize: Sizes.iconSize, this.padding: const EdgeInsets.fromLTRB( const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key);
Sizes.leftWidgetPadding, 0.0, 12.0, 0.0)}) : super(key: key);
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
String domain = entityId.split(".")[0];
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
String iconNameByDeviceClass;
if (deviceClass != null) {
iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"];
}
String iconName = iconNameByDeviceClass ?? iconNameByDomain;
if (iconName != null) {
return MaterialDesignIcons.iconsDataMap[iconName] ?? 0;
} else {
return 0;
}
}
Widget buildIcon(EntityWrapper data, Color color) {
if (data == null) {
return null;
}
if (data.entityPicture != null) {
return Container(
height: size+12,
width: size+12,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit:BoxFit.cover,
image: CachedNetworkImageProvider(
"${data.entityPicture}"
),
)
),
);
}
String iconName = data.icon;
int iconCode = 0;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); //
}
return Padding(
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
child: Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
)
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return Padding( return Padding(
padding: padding, padding: padding,
child: MaterialDesignIcons.createIconWidgetFromEntityData( child: buildIcon(
entityWrapper, entityWrapper,
iconSize, color ?? EntityColor.stateColor(entityWrapper.entity.state)
EntityColor.stateColor(entityWrapper.entity.state)
), ),
); );
} }

View File

@ -14,6 +14,10 @@ class EntityName extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
TextStyle textStyle = TextStyle(fontSize: fontSize);
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline);
}
return Padding( return Padding(
padding: padding, padding: padding,
child: Text( child: Text(
@ -21,7 +25,7 @@ class EntityName extends StatelessWidget {
overflow: textOverflow, overflow: textOverflow,
softWrap: wordsWrap, softWrap: wordsWrap,
maxLines: maxLines, maxLines: maxLines,
style: TextStyle(fontSize: fontSize), style: textStyle,
textAlign: textAlign, textAlign: textAlign,
), ),
); );

View File

@ -22,6 +22,12 @@ class GlanceEntityContainer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> result = []; List<Widget> result = [];
if (!nameInTheBottom) { if (!nameInTheBottom) {
if (showName) { if (showName) {
@ -35,7 +41,7 @@ class GlanceEntityContainer extends StatelessWidget {
result.add( result.add(
EntityIcon( EntityIcon(
padding: EdgeInsets.all(0.0), padding: EdgeInsets.all(0.0),
iconSize: iconSize, size: iconSize,
) )
); );
if (!nameInTheBottom) { if (!nameInTheBottom) {

View File

@ -32,6 +32,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
List _history; List _history;
bool _needToUpdateHistory; bool _needToUpdateHistory;
DateTime _historyLastUpdated; DateTime _historyLastUpdated;
bool _disposed = false;
@override @override
void initState() { void initState() {
@ -46,17 +47,21 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
} }
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) { if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
_historyLastUpdated = now; _historyLastUpdated = now;
ha.getHistory(entityId).then((history){ ha.connection.getHistory(entityId).then((history){
setState(() { if (!_disposed) {
_history = history.isNotEmpty ? history[0] : []; setState(() {
_needToUpdateHistory = false; _history = history.isNotEmpty ? history[0] : [];
}); _needToUpdateHistory = false;
});
}
}).catchError((e) { }).catchError((e) {
Logger.e("Error loading $entityId history: $e"); Logger.e("Error loading $entityId history: $e");
setState(() { if (!_disposed) {
_history = []; setState(() {
_needToUpdateHistory = false; _history = [];
}); _needToUpdateHistory = false;
});
}
}); });
} }
} }
@ -131,4 +136,10 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
} }
@override
void dispose() {
_disposed = true;
super.dispose();
}
} }

View File

@ -0,0 +1,19 @@
part of '../main.dart';
class MissedEntityWidget extends StatelessWidget {
MissedEntityWidget({
Key key
}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
return Container(
child: Padding(
padding: EdgeInsets.all(5.0),
child: Text("Entity not available: ${entityModel.entityWrapper.entity.entityId}"),
),
color: Colors.amber[100],
);
}
}

View File

@ -24,7 +24,7 @@ class CoverStateWidget extends StatelessWidget {
if (entity.supportOpen) { if (entity.supportOpen) {
buttons.add(IconButton( buttons.add(IconButton(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-up"), MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-up"),
size: Sizes.iconSize, size: Sizes.iconSize,
), ),
onPressed: entity.canBeOpened ? () => _open(entity) : null)); onPressed: entity.canBeOpened ? () => _open(entity) : null));
@ -36,7 +36,7 @@ class CoverStateWidget extends StatelessWidget {
if (entity.supportStop) { if (entity.supportStop) {
buttons.add(IconButton( buttons.add(IconButton(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"), MaterialDesignIcons.getIconDataFromIconName("mdi:stop"),
size: Sizes.iconSize, size: Sizes.iconSize,
), ),
onPressed: () => _stop(entity))); onPressed: () => _stop(entity)));
@ -48,7 +48,7 @@ class CoverStateWidget extends StatelessWidget {
if (entity.supportClose) { if (entity.supportClose) {
buttons.add(IconButton( buttons.add(IconButton(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-down"), MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-down"),
size: Sizes.iconSize, size: Sizes.iconSize,
), ),
onPressed: entity.canBeClosed ? () => _close(entity) : null)); onPressed: entity.canBeClosed ? () => _close(entity) : null));

View File

@ -2,6 +2,10 @@ part of '../../main.dart';
class LockStateWidget extends StatelessWidget { class LockStateWidget extends StatelessWidget {
final bool assumedState;
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
void _lock(Entity entity) { void _lock(Entity entity) {
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null)); eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null));
} }
@ -14,19 +18,49 @@ class LockStateWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final entityModel = EntityModel.of(context); final entityModel = EntityModel.of(context);
final LockEntity entity = entityModel.entityWrapper.entity; final LockEntity entity = entityModel.entityWrapper.entity;
return SizedBox( if (assumedState) {
height: 34.0, return Row(
child: FlatButton( mainAxisSize: MainAxisSize.min,
onPressed: (() { children: <Widget>[
entity.isLocked ? _unlock(entity) : _lock(entity); SizedBox(
}), height: 34.0,
child: Text( child: FlatButton(
entity.isLocked ? "UNLOCK" : "LOCK", onPressed: () => _unlock(entity),
textAlign: TextAlign.right, child: Text("UNLOCK",
style: textAlign: TextAlign.right,
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue), style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
),
)
), ),
) SizedBox(
); height: 34.0,
child: FlatButton(
onPressed: () => _lock(entity),
child: Text("LOCK",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
),
)
)
],
);
} else {
return SizedBox(
height: 34.0,
child: FlatButton(
onPressed: (() {
entity.isLocked ? _unlock(entity) : _lock(entity);
}),
child: Text(
entity.isLocked ? "UNLOCK" : "LOCK",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
),
)
);
}
} }
} }

View File

@ -6,14 +6,26 @@ class SimpleEntityState extends StatelessWidget {
final TextAlign textAlign; final TextAlign textAlign;
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
final int maxLines; final int maxLines;
final String customValue;
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0)}) : super(key: key); const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final entityModel = EntityModel.of(context); final entityModel = EntityModel.of(context);
String state = entityModel.entityWrapper.entity.displayState ?? ""; String state;
state = state.replaceAll("\n", "").replaceAll("\t", " ").trim(); if (customValue == null) {
state = entityModel.entityWrapper.entity.displayState ?? "";
state = state.replaceAll("\n", "").replaceAll("\t", " ").trim();
} else {
state = customValue;
}
TextStyle textStyle = TextStyle(
fontSize: Sizes.stateFontSize,
);
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
textStyle = textStyle.apply(color: Colors.blue);
}
while (state.contains(" ")){ while (state.contains(" ")){
state = state.replaceAll(" ", " "); state = state.replaceAll(" ", " ");
} }
@ -25,9 +37,7 @@ class SimpleEntityState extends StatelessWidget {
maxLines: maxLines, maxLines: maxLines,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
softWrap: true, softWrap: true,
style: new TextStyle( style: textStyle
fontSize: Sizes.stateFontSize,
)
) )
); );
if (expanded) { if (expanded) {

View File

@ -71,13 +71,13 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
children: <Widget>[ children: <Widget>[
IconButton( IconButton(
onPressed: () => _setNewState(false, entity), onPressed: () => _setNewState(false, entity),
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash-off")), icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash-off")),
color: newState == EntityState.on ? Colors.black : Colors.blue, color: newState == EntityState.on ? Colors.black : Colors.blue,
iconSize: Sizes.iconSize, iconSize: Sizes.iconSize,
), ),
IconButton( IconButton(
onPressed: () => _setNewState(true, entity), onPressed: () => _setNewState(true, entity),
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash")), icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash")),
color: newState == EntityState.on ? Colors.blue : Colors.black, color: newState == EntityState.on ? Colors.blue : Colors.black,
iconSize: Sizes.iconSize iconSize: Sizes.iconSize
) )

View File

@ -0,0 +1,65 @@
part of '../../main.dart';
class TimerState extends StatefulWidget {
//final bool expanded;
//final TextAlign textAlign;
//final EdgeInsetsGeometry padding;
//final int maxLines;
const TimerState({Key key}) : super(key: key);
@override
_TimerStateState createState() => _TimerStateState();
}
class _TimerStateState extends State<TimerState> {
Timer timer;
Duration remaining = Duration(seconds: 0);
void checkState(TimerEntity entity) {
if (entity.state == EntityState.active) {
//Logger.d("Timer is active");
if (timer == null || !timer.isActive) {
timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
try {
int passed = DateTime
.now()
.difference(entity._lastUpdated)
.inSeconds;
remaining = Duration(seconds: entity.duration.inSeconds - passed);
} catch (e) {
Logger.e("Error calculating ${entity.entityId} remaining time: ${e.toString()}");
remaining = Duration(seconds: 0);
}
});
});
}
} else {
timer?.cancel();
}
}
@override
Widget build(BuildContext context) {
EntityModel model = EntityModel.of(context);
TimerEntity entity = model.entityWrapper.entity;
checkState(entity);
if (entity.state != EntityState.active) {
return SimpleEntityState();
} else {
return SimpleEntityState(
customValue: "${remaining.toString().split('.')[0]}",
);
}
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
}

View File

@ -1,43 +1,27 @@
part of 'main.dart'; part of 'main.dart';
class HomeAssistant { class HomeAssistant {
String _webSocketAPIEndpoint;
String _password; final Connection connection = Connection();
bool _useLovelace = false; bool _useLovelace = false;
//bool isSettingsLoaded = false;
IOWebSocketChannel _hassioChannel;
SendMessageQueue _messageQueue;
int _currentMessageId = 0;
int _statesMessageId = 0;
int _servicesMessageId = 0;
int _subscriptionMessageId = 0;
int _configMessageId = 0;
int _userInfoMessageId = 0;
int _lovelaceMessageId = 0;
EntityCollection entities; EntityCollection entities;
HomeAssistantUI ui; HomeAssistantUI ui;
Map _instanceConfig = {}; Map _instanceConfig = {};
String _userName; String _userName;
String hostname;
HSVColor savedColor;
Map _rawLovelaceData; Map _rawLovelaceData;
Completer _fetchCompleter; List<Panel> panels = [];
Completer _statesCompleter;
Completer _servicesCompleter;
Completer _lovelaceCompleter;
Completer _configCompleter;
Completer _connectionCompleter;
Completer _userInfoCompleter;
Timer _connectionTimer;
Timer _fetchTimer;
bool autoReconnect = false;
StreamSubscription _socketSubscription;
int messageExpirationTime = 30; //seconds
Duration fetchTimeout = Duration(seconds: 30); Duration fetchTimeout = Duration(seconds: 30);
Duration connectTimeout = Duration(seconds: 15);
String get locationName { String get locationName {
if (_useLovelace) { if (_useLovelace) {
@ -48,117 +32,39 @@ class HomeAssistant {
} }
String get userName => _userName ?? locationName; String get userName => _userName ?? locationName;
String get userAvatarText => userName.length > 0 ? userName[0] : ""; String get userAvatarText => userName.length > 0 ? userName[0] : "";
bool get isNoEntities => entities == null || entities.isEmpty;
bool get isNoViews => ui == null || ui.isEmpty;
//int get viewsCount => entities.views.length ?? 0; //int get viewsCount => entities.views.length ?? 0;
HomeAssistant() { HomeAssistant();
entities = EntityCollection();
_messageQueue = SendMessageQueue(messageExpirationTime); Completer _connectCompleter;
Future init() {
if (_connectCompleter != null && !_connectCompleter.isCompleted) {
Logger.w("Previous connection pending...");
return _connectCompleter.future;
}
Logger.d("init...");
_connectCompleter = Completer();
connection.init(_handleEntityStateChange).then((_) {
SharedPreferences.getInstance().then((prefs) {
if (entities == null) entities = EntityCollection(connection.httpWebHost);
_useLovelace = prefs.getBool('use-lovelace') ?? true;
_connectCompleter.complete();
}).catchError((e) => _connectCompleter.completeError(e));
}).catchError((e) => _connectCompleter.completeError(e));
return _connectCompleter.future;
} }
void updateSettings(String url, String password, bool useLovelace) { Completer _fetchCompleter;
_webSocketAPIEndpoint = url;
_password = password;
_useLovelace = useLovelace;
Logger.d( "Use lovelace is $_useLovelace");
}
Future fetch() { Future fetch() {
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) { if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
Logger.w("Previous fetch is not complited"); Logger.w("Previous data fetch is not completed yet");
} else { return _fetchCompleter.future;
_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);
});
} }
return _fetchCompleter.future; _fetchCompleter = Completer();
}
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 {
List<Future> futures = []; List<Future> futures = [];
futures.add(_getStates()); futures.add(_getStates());
if (_useLovelace) { if (_useLovelace) {
@ -167,199 +73,77 @@ class HomeAssistant {
futures.add(_getConfig()); futures.add(_getConfig());
futures.add(_getServices()); futures.add(_getServices());
futures.add(_getUserInfo()); futures.add(_getUserInfo());
try { futures.add(_getPanels());
await Future.wait(futures); Future.wait(futures).then((_) {
_createUI(); _createUI();
_completeFetching(null); _fetchCompleter.complete();
} catch (error) { }).catchError((e) {
_completeFetching(error); _fetchCompleter.completeError(e);
}
}
void _completeFetching(error) {
_fetchTimer.cancel();
_completeConnecting(error);
if (!_fetchCompleter.isCompleted) {
if (error != null) {
_fetchCompleter.completeError(error);
} else {
autoReconnect = true;
Logger.d( "Fetch complete successful");
_fetchCompleter.complete();
}
}
}
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"]));
}
}
}
_handleMessage(String message) {
var data = json.decode(message);
if (data["type"] == "auth_required") {
_sendAuthMessageRaw('{"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'}");
if (data["id"] == _configMessageId) {
_parseConfig(data);
} else if (data["id"] == _statesMessageId) {
_parseEntities(data);
} else if (data["id"] == _lovelaceMessageId) {
_handleLovelace(data);
} else if (data["id"] == _servicesMessageId) {
_parseServices(data);
} else if (data["id"] == _userInfoMessageId) {
_parseUserInfo(data);
}
} 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");
}
} else {
Logger.w("Unknown message type: $message");
}
}
void _sendSubscribe() {
_incrementMessageId();
_subscriptionMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
}
Future _getConfig() {
_configCompleter = new Completer();
_incrementMessageId();
_configMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}', false);
return _configCompleter.future;
}
Future _getStates() {
_statesCompleter = new Completer();
_incrementMessageId();
_statesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}', false);
return _statesCompleter.future;
}
Future _getLovelace() {
_lovelaceCompleter = new Completer();
_incrementMessageId();
_lovelaceMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_lovelaceMessageId, "type": "lovelace/config"}', false);
return _lovelaceCompleter.future;
}
Future _getUserInfo() {
_userInfoCompleter = new Completer();
_incrementMessageId();
_userInfoMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_userInfoMessageId, "type": "auth/current_user"}', false);
return _userInfoCompleter.future;
}
Future _getServices() {
_servicesCompleter = new Completer();
_incrementMessageId();
_servicesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}', false);
return _servicesCompleter.future;
}
_incrementMessageId() {
_currentMessageId += 1;
}
void _sendAuthMessageRaw(String message) {
Logger.d( "[Sending] ==> auth request");
_hassioChannel.sink.add(message);
}
_sendMessageRaw(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);
});
if (!queued) {
Logger.d( "[Sending] ==> $message");
_hassioChannel.sink.add(message);
}
sendCompleter.complete();
}).catchError((e){
sendCompleter.completeError(e);
}); });
return sendCompleter.future; return _fetchCompleter.future;
} }
Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) { Future logout() async {
_incrementMessageId(); Logger.d("Logging out...");
String message = ""; await connection.logout().then((_) {
if (entityId != null) { ui?.clear();
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"'; entities?.clear();
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 += '}'; Future _getConfig() async {
} await connection.sendSocketMessage(type: "get_config").then((data) {
message += '}'; _instanceConfig = Map.from(data);
} }).catchError((e) {
return _sendMessageRaw(message, true); throw {"errorCode": 1, "errorMessage": "Error getting config: $e"};
});
}
Future _getStates() async {
await connection.sendSocketMessage(type: "get_states").then(
(data) => entities.parse(data)
).catchError((e) {
throw {"errorCode": 1, "errorMessage": "Error getting states: $e"};
});
}
Future _getLovelace() async {
await connection.sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
throw {"errorCode": 1, "errorMessage": "Error getting lovelace config: $e"};
});
}
Future _getUserInfo() async {
_userName = null;
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 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 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"]
)
);
});
}).catchError((e) {
throw {"errorCode": 1, "errorMessage": "Error getting panels list: $e"};
});
} }
void _handleEntityStateChange(Map eventData) { void _handleEntityStateChange(Map eventData) {
@ -371,39 +155,6 @@ class HomeAssistant {
)); ));
} }
void _parseConfig(Map data) {
if (data["success"] == true) {
_instanceConfig = Map.from(data["result"]);
_configCompleter.complete();
} else {
_configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]});
}
}
void _parseUserInfo(Map data) {
if (data["success"] == true) {
_userName = data["result"]["name"];
} else {
_userName = null;
Logger.w("There was an error getting current user: $data");
}
_userInfoCompleter.complete();
}
void _parseServices(response) {
_servicesCompleter.complete();
}
void _handleLovelace(response) {
if (response["success"] == true) {
_rawLovelaceData = response["result"];
} else {
Logger.e("There was an error getting Lovelace config: $response");
_rawLovelaceData = null;
}
_lovelaceCompleter.complete();
}
void _parseLovelace() { void _parseLovelace() {
Logger.d("--Title: ${_rawLovelaceData["title"]}"); Logger.d("--Title: ${_rawLovelaceData["title"]}");
ui.title = _rawLovelaceData["title"]; ui.title = _rawLovelaceData["title"];
@ -467,9 +218,51 @@ class HomeAssistant {
if (rawEntity is String) { if (rawEntity is String) {
if (entities.isExist(rawEntity)) { if (entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: entities.get(rawEntity))); card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
} }
} else { } else {
if (entities.isExist(rawEntity["entity"])) { if (rawEntity["type"] == "divider") {
card.entities.add(EntityWrapper(entity: Entity.divider()));
} else if (rawEntity["type"] == "section") {
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
} else if (rawEntity["type"] == "call-service") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.callService,
"service": rawEntity["service"],
"service_data": rawEntity["service_data"]
},
"hold_action": EntityUIAction.none
};
card.entities.add(EntityWrapper(
entity: Entity.callService(
icon: rawEntity["icon"],
name: rawEntity["name"],
service: rawEntity["service"],
actionName: rawEntity["action_name"]
),
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (rawEntity["type"] == "weblink") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.navigate,
"service": rawEntity["url"]
},
"hold_action": EntityUIAction.none
};
card.entities.add(EntityWrapper(
entity: Entity.weblink(
icon: rawEntity["icon"],
name: rawEntity["name"],
url: rawEntity["url"]
),
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (entities.isExist(rawEntity["entity"])) {
Entity e = entities.get(rawEntity["entity"]); Entity e = entities.get(rawEntity["entity"]);
card.entities.add( card.entities.add(
EntityWrapper( EntityWrapper(
@ -479,6 +272,8 @@ class HomeAssistant {
uiAction: EntityUIAction(rawEntityData: rawEntity) uiAction: EntityUIAction(rawEntityData: rawEntity)
) )
); );
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
} }
} }
}); });
@ -493,6 +288,8 @@ class HomeAssistant {
displayName: rawCard["name"], displayName: rawCard["name"],
uiAction: EntityUIAction(rawEntityData: rawCard) uiAction: EntityUIAction(rawEntityData: rawCard)
); );
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
} }
} else { } else {
if (entities.isExist(en["entity"])) { if (entities.isExist(en["entity"])) {
@ -503,6 +300,8 @@ class HomeAssistant {
displayName: en["name"], displayName: en["name"],
uiAction: EntityUIAction(rawEntityData: rawCard) uiAction: EntityUIAction(rawEntityData: rawCard)
); );
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
} }
} }
} }
@ -514,15 +313,6 @@ class HomeAssistant {
return result; return result;
} }
void _parseEntities(response) async {
if (response["success"] == false) {
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
return;
}
entities.parse(response["result"]);
_statesCompleter.complete();
}
void _createUI() { void _createUI() {
ui = HomeAssistantUI(); ui = HomeAssistantUI();
if ((_useLovelace) && (_rawLovelaceData != null)) { if ((_useLovelace) && (_rawLovelaceData != null)) {
@ -559,31 +349,12 @@ class HomeAssistant {
} }
} }
Widget buildViews(BuildContext context, bool lovelace) { Widget buildViews(BuildContext context, TabController tabController) {
return ui.build(context); 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 { class SendMessageQueue {
int _messageTimeout; int _messageTimeout;
List<HAMessage> _queue = []; List<HAMessage> _queue = [];
@ -622,4 +393,4 @@ class HAMessage {
bool isExpired() { bool isExpired() {
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout; return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
} }
} }*/

View File

@ -8,7 +8,7 @@ import 'package:web_socket_channel/io.dart';
import 'package:event_bus/event_bus.dart'; import 'package:event_bus/event_bus.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart' as urlLauncher;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:date_format/date_format.dart'; import 'package:date_format/date_format.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -16,10 +16,15 @@ import 'package:charts_flutter/flutter.dart' as charts;
import 'package:progress_indicators/progress_indicators.dart'; import 'package:progress_indicators/progress_indicators.dart';
import 'package:flutter_markdown/flutter_markdown.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';
part 'entity_class/const.dart'; part 'entity_class/const.dart';
part 'entity_class/entity.class.dart'; part 'entity_class/entity.class.dart';
part 'entity_class/entity_wrapper.class.dart'; part 'entity_class/entity_wrapper.class.dart';
part 'entity_class/timer_entity.dart';
part 'entity_class/switch_entity.class.dart'; part 'entity_class/switch_entity.class.dart';
part 'entity_class/button_entity.class.dart'; part 'entity_class/button_entity.class.dart';
part 'entity_class/text_entity.class.dart'; part 'entity_class/text_entity.class.dart';
@ -40,6 +45,7 @@ part 'entity_class/alarm_control_panel.class.dart';
part 'entity_widgets/common/badge.dart'; part 'entity_widgets/common/badge.dart';
part 'entity_widgets/model_widgets.dart'; part 'entity_widgets/model_widgets.dart';
part 'entity_widgets/default_entity_container.dart'; part 'entity_widgets/default_entity_container.dart';
part 'entity_widgets/missed_entity.dart';
part 'entity_widgets/glance_entity_container.dart'; part 'entity_widgets/glance_entity_container.dart';
part 'entity_widgets/button_entity_container.dart'; part 'entity_widgets/button_entity_container.dart';
part 'entity_widgets/common/entity_attributes_list.dart'; part 'entity_widgets/common/entity_attributes_list.dart';
@ -65,6 +71,7 @@ part 'entity_widgets/controls/slider_controls.dart';
part 'entity_widgets/state/text_input_state.dart'; part 'entity_widgets/state/text_input_state.dart';
part 'entity_widgets/state/select_state.dart'; part 'entity_widgets/state/select_state.dart';
part 'entity_widgets/state/simple_state.dart'; part 'entity_widgets/state/simple_state.dart';
part 'entity_widgets/state/timer_state.dart';
part 'entity_widgets/state/climate_state.dart'; part 'entity_widgets/state/climate_state.dart';
part 'entity_widgets/state/cover_state.dart'; part 'entity_widgets/state/cover_state.dart';
part 'entity_widgets/state/date_time_state.dart'; part 'entity_widgets/state/date_time_state.dart';
@ -76,27 +83,29 @@ part 'entity_widgets/controls/media_player_widgets.dart';
part 'entity_widgets/controls/fan_controls.dart'; part 'entity_widgets/controls/fan_controls.dart';
part 'entity_widgets/controls/alarm_control_panel_controls.dart'; part 'entity_widgets/controls/alarm_control_panel_controls.dart';
part 'settings.page.dart'; part 'settings.page.dart';
part 'configuration.page.dart'; part 'panel.page.dart';
part 'home_assistant.class.dart'; part 'home_assistant.class.dart';
part 'log.page.dart'; part 'log.page.dart';
part 'entity.page.dart'; part 'entity.page.dart';
part 'utils.class.dart'; part 'utils.class.dart';
part 'mdi.class.dart'; part 'mdi.class.dart';
part 'entity_collection.class.dart'; part 'entity_collection.class.dart';
part 'auth_manager.class.dart';
part 'connection.class.dart';
part 'ui_class/ui.dart'; part 'ui_class/ui.dart';
part 'ui_class/view.class.dart'; part 'ui_class/view.class.dart';
part 'ui_class/card.class.dart'; part 'ui_class/card.class.dart';
part 'ui_class/sizes_class.dart'; part 'ui_class/sizes_class.dart';
part 'ui_class/panel_class.dart';
part 'ui_widgets/view.dart'; part 'ui_widgets/view.dart';
part 'ui_widgets/card_widget.dart'; part 'ui_widgets/card_widget.dart';
part 'ui_widgets/card_header_widget.dart'; part 'ui_widgets/card_header_widget.dart';
part 'ui_widgets/config_panel_widget.dart';
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
const String appName = "HA Client"; const String appName = "HA Client";
const appVersion = "0.4.4"; const appVersion = "0.6.0-alpha1";
String homeAssistantWebHost;
void main() { void main() {
FlutterError.onError = (errorDetails) { FlutterError.onError = (errorDetails) {
@ -118,6 +127,8 @@ void main() {
} }
class HAClientApp extends StatelessWidget { class HAClientApp extends StatelessWidget {
final HomeAssistant homeAssistant = HomeAssistant();
// This widget is the root of your application. // This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -128,9 +139,9 @@ class HAClientApp extends StatelessWidget {
), ),
initialRoute: "/", initialRoute: "/",
routes: { routes: {
"/": (context) => MainPage(title: 'HA Client'), "/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"), "/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
"/configuration": (context) => ConfigurationPage(title: "Configuration"), "/configuration": (context) => PanelPage(title: "Configuration"),
"/log-view": (context) => LogViewPage(title: "Log") "/log-view": (context) => LogViewPage(title: "Log")
}, },
); );
@ -138,81 +149,84 @@ class HAClientApp extends StatelessWidget {
} }
class MainPage extends StatefulWidget { 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 String title;
final HomeAssistant homeAssistant;
@override @override
_MainPageState createState() => new _MainPageState(); _MainPageState createState() => new _MainPageState();
} }
class _MainPageState extends State<MainPage> with WidgetsBindingObserver { class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
HomeAssistant _homeAssistant;
//Map _instanceConfig;
String _webSocketApiEndpoint;
String _password;
//int _uiViewsCount = 0;
String _instanceHost;
StreamSubscription _stateSubscription; StreamSubscription _stateSubscription;
StreamSubscription _settingsSubscription; StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription; StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription; StreamSubscription _showEntityPageSubscription;
StreamSubscription _showErrorSubscription; StreamSubscription _showErrorSubscription;
bool _settingsLoaded = false; StreamSubscription _startAuthSubscription;
bool _accountMenuExpanded = false; StreamSubscription _reloadUISubscription;
bool _useLovelaceUI; int _previousViewCount;
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_settingsLoaded = false; //widget.homeAssistant = HomeAssistant();
//_settingsLoaded = false;
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
Logger.d("<!!!> Creating new HomeAssistant instance");
_homeAssistant = HomeAssistant();
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) { _settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
Logger.d("Settings change event: reconnect=${event.reconnect}"); Logger.d("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) { if (event.reconnect) {
_homeAssistant.disconnect().then((_){ _reLoad();
_initialLoad();
});
} }
}); });
_initialLoad(); _initialLoad();
} }
void _initialLoad() { void _initialLoad() {
_loadConnectionSettings().then((_){ _showInfoBottomBar(progress: true,);
_subscribe(); _subscribe();
_refreshData(); widget.homeAssistant.init().then((_){
}, onError: (_) { _fetchData();
_showErrorBottomBar(message: _, errorCode: 5); }, onError: (e) {
_setErrorState(e);
}); });
} }
void _reLoad() {
_hideBottomBar();
_showInfoBottomBar(progress: true,);
widget.homeAssistant.init().then((_){
_fetchData();
}, onError: (e) {
_setErrorState(e);
});
}
_fetchData() async {
await widget.homeAssistant.fetch().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) {
_setErrorState(e);
});
eventBus.fire(RefreshDataFinishedEvent());
}
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
Logger.d("$state"); Logger.d("$state");
if (state == AppLifecycleState.resumed && _settingsLoaded) { if (state == AppLifecycleState.resumed) {
_refreshData(); _reLoad();
}
}
_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;
} }
} }
@ -221,12 +235,17 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) { if (event.needToRebuildUI) {
Logger.d("New entity. Need to rebuild UI"); Logger.d("New entity. Need to rebuild UI");
_refreshData(); _reLoad();
} else { } else {
setState(() {}); setState(() {});
} }
}); });
} }
if (_reloadUISubscription == null) {
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
_reLoad();
});
}
if (_serviceCallSubscription == null) { if (_serviceCallSubscription == null) {
_serviceCallSubscription = _serviceCallSubscription =
eventBus.on<ServiceCallEvent>().listen((event) { eventBus.on<ServiceCallEvent>().listen((event) {
@ -247,18 +266,51 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_showErrorBottomBar(message: event.text, errorCode: event.errorCode); _showErrorBottomBar(message: event.text, errorCode: event.errorCode);
}); });
} }
if (_startAuthSubscription == null) {
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
_showOAuth();
});
}
/*_firebaseMessaging.getToken().then((String token) {
//Logger.d("FCM token: $token");
widget.homeAssistant.sendHTTPPost(
endPoint: '/api/notify.fcm-android',
jsonData: '{"token": "$token"}'
);
});
_firebaseMessaging.configure(
onLaunch: (data) {
Logger.d("Notification [onLaunch]: $data");
},
onMessage: (data) {
Logger.d("Notification [onMessage]: $data");
},
onResume: (data) {
Logger.d("Notification [onResume]: $data");
}
);*/
} }
_refreshData() async { void _showOAuth() {
_homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI); Navigator.push(
_hideBottomBar(); context,
_showInfoBottomBar(progress: true,); MaterialPageRoute(
await _homeAssistant.fetch().then((result) { builder: (context) => WebviewScaffold(
_hideBottomBar(); url: "${widget.homeAssistant.connection.oauthUrl}",
}).catchError((e) { appBar: new AppBar(
_setErrorState(e); leading: IconButton(
}); icon: Icon(Icons.help),
eventBus.fire(RefreshDataFinishedEvent()); onPressed: () => HAUtils.launchURLInCustomTab(context, "http://ha-client.homemade.systems/docs#authentication")
),
title: new Text("Login to your Home Assistant"),
),
),
)
);
} }
_setErrorState(e) { _setErrorState(e) {
@ -266,7 +318,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
Logger.e(e.toString()); Logger.e(e.toString());
Logger.e("${e.stackTrace}"); Logger.e("${e.stackTrace}");
_showErrorBottomBar( _showErrorBottomBar(
message: "There was some error", message: "Unknown error",
errorCode: 13 errorCode: 13
); );
} else { } else {
@ -277,19 +329,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
} }
} }
void _callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) { void _callService(String domain, String service, String entityId, Map additionalParams) {
_showInfoBottomBar( _showInfoBottomBar(
message: "Calling $domain.$service", message: "Calling $domain.$service",
duration: Duration(seconds: 3) duration: Duration(seconds: 3)
); );
_homeAssistant.callService(domain, service, entityId, additionalParams).catchError((e) => _setErrorState(e)); widget.homeAssistant.connection.callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
} }
void _showEntityPage(String entityId) { void _showEntityPage(String entityId) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: _homeAssistant), builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: widget.homeAssistant),
) )
); );
} }
@ -297,8 +349,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
List<Tab> buildUIViewTabs() { List<Tab> buildUIViewTabs() {
List<Tab> result = []; List<Tab> result = [];
if (_homeAssistant.ui.views.isNotEmpty) { if (widget.homeAssistant.ui.views.isNotEmpty) {
_homeAssistant.ui.views.forEach((HAView view) { widget.homeAssistant.ui.views.forEach((HAView view) {
result.add(view.buildTab()); result.add(view.buildTab());
}); });
} }
@ -310,16 +362,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
List<Widget> menuItems = []; List<Widget> menuItems = [];
menuItems.add( menuItems.add(
UserAccountsDrawerHeader( UserAccountsDrawerHeader(
accountName: Text(_homeAssistant.userName), accountName: Text(widget.homeAssistant.userName),
accountEmail: Text(_instanceHost ?? "Not configured"), accountEmail: Text(widget.homeAssistant.hostname ?? "Not configured"),
onDetailsPressed: () { /*onDetailsPressed: () {
setState(() { setState(() {
_accountMenuExpanded = !_accountMenuExpanded; _accountMenuExpanded = !_accountMenuExpanded;
}); });
}, },*/
currentAccountPicture: CircleAvatar( currentAccountPicture: CircleAvatar(
child: Text( child: Text(
_homeAssistant.userAvatarText, widget.homeAssistant.userAvatarText,
style: TextStyle( style: TextStyle(
fontSize: 32.0 fontSize: 32.0
), ),
@ -327,28 +379,40 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
), ),
) )
); );
if (_accountMenuExpanded) { 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)
)
);
}
});
}
//TODO check for loaded
menuItems.add(
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
title: Text("Open Web UI"),
onTap: () => HAUtils.launchURL(widget.homeAssistant.connection.httpWebHost),
)
);
menuItems.addAll([ menuItems.addAll([
ListTile(
leading: Icon(Icons.settings),
title: Text("Settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/connection-settings');
},
),
Divider(), Divider(),
]); ListTile(
} else { leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
menuItems.addAll([ title: Text("Connection settings"),
new ListTile(
leading: Icon(Icons.settings),
title: Text("Configuration"),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).pushNamed('/configuration'); Navigator.of(context).pushNamed('/connection-settings', arguments: {"homeAssistant", widget.homeAssistant});
}, },
), )
]);
menuItems.addAll([
Divider(),
new ListTile( new ListTile(
leading: Icon(Icons.insert_drive_file), leading: Icon(Icons.insert_drive_file),
title: Text("Log"), title: Text("Log"),
@ -358,7 +422,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}, },
), ),
new ListTile( new ListTile(
leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")), leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
title: Text("Report an issue"), title: Text("Report an issue"),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -366,6 +430,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}, },
), ),
Divider(), Divider(),
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
title: Text("Join Discord server"),
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURL("https://discord.gg/AUzEvwn");
},
),
new AboutListTile( new AboutListTile(
aboutBoxChildren: <Widget>[ aboutBoxChildren: <Widget>[
GestureDetector( GestureDetector(
@ -386,7 +458,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
applicationVersion: appVersion applicationVersion: appVersion
) )
]); ]);
}
return new Drawer( return new Drawer(
child: ListView( child: ListView(
children: menuItems, children: menuItems,
@ -439,7 +510,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
child: Text("Retry", style: textStyle), child: Text("Retry", style: textStyle),
onPressed: () { onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar(); //_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData(); _reLoad();
}, },
); );
break; break;
@ -457,12 +528,32 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
break; break;
} }
case 6: { case 60: {
_bottomBarAction = FlatButton( _bottomBarAction = FlatButton(
child: Text("Settings", style: textStyle), child: Text("Login", style: textStyle),
onPressed: () { onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar(); _reLoad();
Navigator.pushNamed(context, '/connection-settings'); },
);
break;
}
case 63:
case 61: {
_bottomBarAction = FlatButton(
child: Text("Try again", style: textStyle),
onPressed: () {
_reLoad();
},
);
break;
}
case 62: {
_bottomBarAction = FlatButton(
child: Text("Login again", style: textStyle),
onPressed: () {
_reLoad();
}, },
); );
break; break;
@ -473,52 +564,51 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
child: Text("Refresh", style: textStyle), child: Text("Refresh", style: textStyle),
onPressed: () { onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar(); //_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData(); _reLoad();
}, },
); );
break; break;
} }
case 82:
case 81:
case 8: { case 8: {
_bottomBarAction = FlatButton( _bottomBarAction = FlatButton(
child: Text("Reconnect", style: textStyle), child: Text("Reconnect", style: textStyle),
onPressed: () { onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar(); _reLoad();
_refreshData();
}, },
); );
break; break;
} }
default: { default: {
_bottomBarAction = FlatButton( _bottomBarAction = Container(width: 0.0, height: 0.0,);
child: Text("Reload", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
},
);
break; break;
} }
} }
setState(() { setState(() {
_bottomBarProgress = false; _bottomBarProgress = false;
_bottomBarText = "$message (code: $errorCode)"; _bottomBarText = "$message";
_showBottomBar = true; _showBottomBar = true;
}); });
/*_scaffoldKey.currentState.hideCurrentSnackBar();
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text("$message (code: $errorCode)"),
action: action,
duration: Duration(hours: 1),
)
);*/
} }
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget _buildScaffoldBody(bool empty) { Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> popupMenuItems = [];
popupMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
));
if (widget.homeAssistant.connection.isAuthenticated) {
popupMenuItems.add(
PopupMenuItem<String>(
child: new Text("Logout"),
value: "logout",
));
}
return NestedScrollView( return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[ return <Widget>[
@ -526,22 +616,23 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
floating: true, floating: true,
pinned: true, pinned: true,
primary: true, primary: true,
title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""), title: Text(widget.homeAssistant.locationName ?? ""),
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Colors.white,), "mdi:dots-vertical"), color: Colors.white,),
onPressed: () { onPressed: () {
showMenu( showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0), position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
context: context, context: context,
items: [PopupMenuItem<String>( items: popupMenuItems
child: new Text("Reload"),
value: "reload",
)]
).then((String val) { ).then((String val) {
if (val == "reload") { if (val == "reload") {
_refreshData(); _reLoad();
} else if (val == "logout") {
widget.homeAssistant.logout().then((_) {
_reLoad();
});
} }
}); });
} }
@ -551,12 +642,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
icon: Icon(Icons.menu), icon: Icon(Icons.menu),
onPressed: () { onPressed: () {
_scaffoldKey.currentState.openDrawer(); _scaffoldKey.currentState.openDrawer();
setState(() {
_accountMenuExpanded = false;
});
}, },
), ),
bottom: empty ? null : TabBar( bottom: empty ? null : TabBar(
controller: _viewsTabController,
tabs: buildUIViewTabs(), tabs: buildUIViewTabs(),
isScrollable: true, isScrollable: true,
), ),
@ -570,18 +659,20 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"), MaterialDesignIcons.getIconDataFromIconName("mdi:border-none-variant"),
size: 100.0, size: 100.0,
color: Colors.blue, color: Colors.black26,
), ),
] ]
), ),
) )
: :
_homeAssistant.buildViews(context, _useLovelaceUI), widget.homeAssistant.buildViews(context, _viewsTabController),
); );
} }
TabController _viewsTabController;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget bottomBar; Widget bottomBar;
@ -633,7 +724,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
} }
} }
// This method is rerun every time setState is called. // This method is rerun every time setState is called.
if (_homeAssistant.ui == null || _homeAssistant.ui.views == null) { if (widget.homeAssistant.isNoViews) {
return Scaffold( return Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
primary: false, primary: false,
@ -647,9 +738,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
drawer: _buildAppDrawer(), drawer: _buildAppDrawer(),
primary: false, primary: false,
bottomNavigationBar: bottomBar, bottomNavigationBar: bottomBar,
body: DefaultTabController( body: HomeAssistantModel(
length: _homeAssistant.ui?.views?.length ?? 0,
child: _buildScaffoldBody(false), child: _buildScaffoldBody(false),
homeAssistant: widget.homeAssistant
), ),
); );
} }
@ -657,13 +748,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
@override @override
void dispose() { void dispose() {
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.dispose();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
if (_stateSubscription != null) _stateSubscription.cancel(); _viewsTabController?.dispose();
if (_settingsSubscription != null) _settingsSubscription.cancel(); _stateSubscription?.cancel();
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel(); _settingsSubscription?.cancel();
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel(); _serviceCallSubscription?.cancel();
if (_showErrorSubscription != null) _showErrorSubscription.cancel(); _showEntityPageSubscription?.cancel();
_homeAssistant.disconnect(); _showErrorSubscription?.cancel();
_startAuthSubscription?.cancel();
_reloadUISubscription?.cancel();
//TODO disconnect
//widget.homeAssistant?.disconnect();
super.dispose(); super.dispose();
} }
} }

File diff suppressed because it is too large Load Diff

40
lib/panel.page.dart Normal file
View File

@ -0,0 +1,40 @@
part of 'main.dart';
class PanelPage extends StatefulWidget {
PanelPage({Key key, this.title, this.panel}) : super(key: key);
final String title;
final Panel panel;
@override
_PanelPageState createState() => new _PanelPageState();
}
class _PanelPageState extends State<PanelPage> {
List<ConfigurationItem> _items;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
title: new Text(widget.title),
),
body: widget.panel.getWidget(),
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -14,17 +14,18 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _newHassioDomain = ""; String _newHassioDomain = "";
String _hassioPort = ""; String _hassioPort = "";
String _newHassioPort = ""; String _newHassioPort = "";
String _hassioPassword = "";
String _newHassioPassword = "";
String _socketProtocol = "wss"; String _socketProtocol = "wss";
String _newSocketProtocol = "wss"; String _newSocketProtocol = "wss";
bool _useLovelace = true; bool _useLovelace = true;
bool _newUseLovelace = true; bool _newUseLovelace = true;
String oauthUrl;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadSettings(); _loadSettings();
} }
_loadSettings() async { _loadSettings() async {
@ -33,7 +34,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
setState(() { setState(() {
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? ""; _hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? ""; _hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss'; _socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
try { try {
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true; _useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
@ -44,7 +44,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
} }
bool _checkConfigChanged() { bool _checkConfigChanged() {
return ((_newHassioPassword != _hassioPassword) || return (
(_newHassioPort != _hassioPort) || (_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) || (_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) || (_newSocketProtocol != _socketProtocol) ||
@ -59,7 +59,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("hassio-domain", _newHassioDomain); prefs.setString("hassio-domain", _newHassioDomain);
prefs.setString("hassio-port", _newHassioPort); prefs.setString("hassio-port", _newHassioPort);
prefs.setString("hassio-password", _newHassioPassword);
prefs.setString("hassio-protocol", _newSocketProtocol); prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http"); prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
prefs.setBool("use-lovelace", _newUseLovelace); 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.", "Try ports 80 and 443 if default is not working and you don't know why.",
style: TextStyle(color: Colors.grey), 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(
padding: EdgeInsets.only(top: 20.0), padding: EdgeInsets.only(top: 20.0),
child: Text( child: Text(

View File

@ -0,0 +1,58 @@
part of '../main.dart';
class Panel {
static const iconsByComponent = {
"config": "mdi:settings",
"history": "mdi:poll-box",
"map": "mdi:tooltip-account",
"logbook": "mdi:format-list-bulleted-type",
"custom": "mdi:home-assistant"
};
final String id;
final String type;
final String title;
final String urlPath;
final Map config;
String icon;
bool isHidden = true;
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
if (icon == null || !icon.startsWith("mdi:")) {
icon = Panel.iconsByComponent[type];
}
isHidden = (type != "iframe" && type != "config");
}
void handleOpen(BuildContext context) {
if (type == "iframe") {
Logger.d("Launching custom tab with ${config["url"]}");
HAUtils.launchURLInCustomTab(context, config["url"]);
} else if (type == "config") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PanelPage(title: "$title", panel: this),
)
);
} else {
HomeAssistantModel haModel = HomeAssistantModel.of(context);
String url = "${haModel.homeAssistant.connection.httpWebHost}/$urlPath";
Logger.d("Launching custom tab with $url");
HAUtils.launchURLInCustomTab(context, url);
}
}
Widget getWidget() {
switch (type) {
case "config": {
return ConfigPanelWidget();
}
default: {
return Text("Unsupported panel component: $type");
}
}
}
}

View File

@ -4,12 +4,15 @@ class HomeAssistantUI {
List<HAView> views; List<HAView> views;
String title; String title;
bool get isEmpty => views == null || views.isEmpty;
HomeAssistantUI() { HomeAssistantUI() {
views = []; views = [];
} }
Widget build(BuildContext context) { Widget build(BuildContext context, TabController tabController) {
return TabBarView( return TabBarView(
controller: tabController,
children: _buildViews(context) children: _buildViews(context)
); );
} }
@ -24,4 +27,8 @@ class HomeAssistantUI {
return result; return result;
} }
void clear() {
views.clear();
}
} }

View File

@ -77,7 +77,7 @@ class HAView {
Tab( Tab(
icon: icon:
Icon( Icon(
MaterialDesignIcons.createIconDataFromIconName( MaterialDesignIcons.getIconDataFromIconName(
iconName ?? "mdi:home-assistant"), iconName ?? "mdi:home-assistant"),
size: 24.0, size: 24.0,
) )
@ -92,7 +92,7 @@ class HAView {
if (linkedEntity.icon != null && linkedEntity.icon.length > 0) { if (linkedEntity.icon != null && linkedEntity.icon.length > 0) {
return Tab( return Tab(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName( MaterialDesignIcons.getIconDataFromIconName(
linkedEntity.icon), linkedEntity.icon),
size: 24.0, size: 24.0,
) )

View File

@ -11,8 +11,17 @@ class CardWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if ((card.linkedEntityWrapper!= null) && (card.linkedEntityWrapper.entity.isHidden)) { if (card.linkedEntityWrapper!= null) {
return Container(width: 0.0, height: 0.0,); if (card.linkedEntityWrapper.entity.isHidden) {
return Container(width: 0.0, height: 0.0,);
}
if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: MissedEntityWidget(),
handleTap: false,
);
}
} }
switch (card.type) { switch (card.type) {
@ -103,7 +112,7 @@ class CardWidget extends StatelessWidget {
if (!entity.entity.isHidden) { if (!entity.entity.isHidden) {
body.add( body.add(
Padding( Padding(
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), padding: EdgeInsets.fromLTRB(10.0, 4.0, 0.0, 4.0),
child: EntityModel( child: EntityModel(
entityWrapper: entity, entityWrapper: entity,
handleTap: true, handleTap: true,
@ -133,55 +142,51 @@ class CardWidget extends StatelessWidget {
} }
Widget _buildAlarmPanelCard(BuildContext context) { Widget _buildAlarmPanelCard(BuildContext context) {
if (card.linkedEntityWrapper == null || card.linkedEntityWrapper.entity == null) { List<Widget> body = [];
return Container(width: 0, height: 0,); body.add(CardHeaderWidget(
} else { name: card.name ?? "",
List<Widget> body = []; subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
body.add(CardHeaderWidget( style: TextStyle(
name: card.name ?? "",
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
style: TextStyle(
color: Colors.grey color: Colors.grey
),
), ),
trailing: Row( ),
trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
EntityIcon( EntityIcon(
iconSize: 50.0, size: 50.0,
), ),
Container( Container(
width: 26.0, width: 26.0,
child: IconButton( child: IconButton(
padding: EdgeInsets.all(0.0), padding: EdgeInsets.all(0.0),
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.createIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")), "mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity)) onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity))
) )
) )
] ]
), ),
)); ));
body.add( body.add(
AlarmControlPanelControlsWidget( AlarmControlPanelControlsWidget(
extended: true, extended: true,
states: card.states, states: card.states,
) )
); );
return Card( return Card(
child: EntityModel( child: EntityModel(
entityWrapper: card.linkedEntityWrapper, entityWrapper: card.linkedEntityWrapper,
handleTap: null, handleTap: null,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: body children: body
) )
) )
); );
}
} }
Widget _buildGlanceCard(BuildContext context) { Widget _buildGlanceCard(BuildContext context) {
@ -227,33 +232,25 @@ class CardWidget extends StatelessWidget {
} }
Widget _buildMediaControlsCard(BuildContext context) { Widget _buildMediaControlsCard(BuildContext context) {
if (card.linkedEntityWrapper == null || card.linkedEntityWrapper.entity == null) { return Card(
return Container(width: 0, height: 0,); child: EntityModel(
} else { entityWrapper: card.linkedEntityWrapper,
return Card( handleTap: null,
child: EntityModel( child: MediaPlayerWidget()
entityWrapper: card.linkedEntityWrapper, )
handleTap: null, );
child: MediaPlayerWidget()
)
);
}
} }
Widget _buildEntityButtonCard(BuildContext context) { Widget _buildEntityButtonCard(BuildContext context) {
if (card.linkedEntityWrapper == null || card.linkedEntityWrapper.entity == null) { card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
return Container(width: 0, height: 0,); card.linkedEntityWrapper.displayName.toUpperCase();
} else { return Card(
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ?? child: EntityModel(
card.linkedEntityWrapper.displayName.toUpperCase(); entityWrapper: card.linkedEntityWrapper,
return Card( child: ButtonEntityContainer(),
child: EntityModel( handleTap: true
entityWrapper: card.linkedEntityWrapper, )
child: ButtonEntityContainer(), );
handleTap: true
)
);
}
} }
Widget _buildUnsupportedCard(BuildContext context) { Widget _buildUnsupportedCard(BuildContext context) {

View File

@ -1,12 +1,10 @@
part of 'main.dart'; part of '../main.dart';
class ConfigurationPage extends StatefulWidget { class ConfigPanelWidget extends StatefulWidget {
ConfigurationPage({Key key, this.title}) : super(key: key); ConfigPanelWidget({Key key}) : super(key: key);
final String title;
@override @override
_ConfigurationPageState createState() => new _ConfigurationPageState(); _ConfigPanelWidgetState createState() => new _ConfigPanelWidgetState();
} }
class ConfigurationItem { class ConfigurationItem {
@ -17,7 +15,7 @@ class ConfigurationItem {
final Widget body; final Widget body;
} }
class _ConfigurationPageState extends State<ConfigurationPage> { class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
List<ConfigurationItem> _items; List<ConfigurationItem> _items;
@ -64,37 +62,29 @@ class _ConfigurationPageState extends State<ConfigurationPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Scaffold( return ListView(
appBar: new AppBar( children: [
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){ new ExpansionPanelList(
Navigator.pop(context); expansionCallback: (int index, bool isExpanded) {
}), setState(() {
title: new Text(widget.title), _items[index].isExpanded = !_items[index].isExpanded;
), });
body: ListView( },
children: [ children: _items.map((ConfigurationItem item) {
new ExpansionPanelList( return new ExpansionPanel(
expansionCallback: (int index, bool isExpanded) { headerBuilder: (BuildContext context, bool isExpanded) {
setState(() { return CardHeaderWidget(
_items[index].isExpanded = !_items[index].isExpanded; name: item.header,
}); );
}, },
children: _items.map((ConfigurationItem item) { isExpanded: item.isExpanded,
return new ExpansionPanel( body: new Container(
headerBuilder: (BuildContext context, bool isExpanded) { child: item.body,
return CardHeaderWidget( ),
name: item.header, );
); }).toList(),
}, ),
isExpanded: item.isExpanded, ],
body: new Container(
child: item.body,
),
);
}).toList(),
),
],
),
); );
} }

View File

@ -47,12 +47,44 @@ class Logger {
class HAUtils { class HAUtils {
static void launchURL(String url) async { static void launchURL(String url) async {
if (await canLaunch(url)) { if (await urlLauncher.canLaunch(url)) {
await launch(url); await urlLauncher.launch(url);
} else { } else {
Logger.e( "Could not launch $url"); Logger.e( "Could not launch $url");
} }
} }
static void launchURLInCustomTab(BuildContext context, String url) async {
try {
await launch(
"$url",
option: new CustomTabsOption(
toolbarColor: Theme.of(context).primaryColor,
enableDefaultShare: true,
enableUrlBarHiding: true,
showPageTitle: true,
animation: new CustomTabsAnimation.slideIn()
// or user defined animation.
/*animation: new CustomTabsAnimation(
startEnter: 'slide_up',
startExit: 'android:anim/fade_out',
endEnter: 'android:anim/fade_in',
endExit: 'slide_down',
)*/,
extraCustomTabs: <String>[
// ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox
'org.mozilla.firefox',
// ref. https://play.google.com/store/apps/details?id=com.microsoft.emmx
'com.microsoft.emmx',
],
),
);
} catch (e) {
Logger.w("Can't open custom tab: ${e.toString()}");
Logger.w("Launching in default browser");
HAUtils.launchURL(url);
}
}
} }
class StateChangedEvent { class StateChangedEvent {
@ -77,6 +109,16 @@ class RefreshDataFinishedEvent {
RefreshDataFinishedEvent(); RefreshDataFinishedEvent();
} }
class ReloadUIEvent {
ReloadUIEvent();
}
class StartAuthEvent {
String oauthUrl;
StartAuthEvent(this.oauthUrl);
}
class ServiceCallEvent { class ServiceCallEvent {
String domain; String domain;
String service; String service;

View File

@ -35,7 +35,7 @@ packages:
name: cached_network_image name: cached_network_image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.0+1" version: "0.7.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -101,6 +101,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "1.0.3"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0+1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -112,7 +119,14 @@ packages:
name: flutter_cache_manager name: flutter_cache_manager
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.0" version: "0.3.2"
flutter_custom_tabs:
dependency: "direct main"
description:
name: flutter_custom_tabs
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.0"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -127,18 +141,32 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.0" version: "0.2.0"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
flutter_svg: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_svg name: flutter_svg
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.10.3" version: "0.10.4"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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.1"
http: http:
dependency: transitive dependency: transitive
description: description:
@ -223,6 +251,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.0+1" version: "0.5.0+1"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@ -230,6 +265,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
progress_indicators: progress_indicators:
dependency: "direct main" dependency: "direct main"
description: description:
@ -250,7 +292,7 @@ packages:
name: shared_preferences name: shared_preferences
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.1+1" version: "0.5.1+2"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -262,14 +304,14 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.4.1" version: "1.5.4"
sqflite: sqflite:
dependency: transitive dependency: transitive
description: description:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0+1" version: "1.1.3"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -297,21 +339,21 @@ packages:
name: synchronized name: synchronized
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2+1" version: "2.1.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
name: term_glyph name: term_glyph
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.1.0"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.1" version: "0.2.2"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -325,7 +367,7 @@ packages:
name: url_launcher name: url_launcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.1" version: "5.0.2"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
@ -363,4 +405,4 @@ packages:
version: "2.1.15" version: "2.1.15"
sdks: sdks:
dart: ">=2.1.0 <3.0.0" dart: ">=2.1.0 <3.0.0"
flutter: ">=0.7.3 <2.0.0" flutter: ">=1.2.1 <2.0.0"

View File

@ -1,7 +1,7 @@
name: hass_client name: hass_client
description: Home Assistant Android Client description: Home Assistant Android Client
version: 0.4.4+95 version: 0.6.0+101
environment: environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0" sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -19,6 +19,10 @@ dependencies:
charts_flutter: any charts_flutter: any
flutter_markdown: 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
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -59,7 +63,7 @@ flutter:
fonts: fonts:
- family: "Material Design Icons" - family: "Material Design Icons"
fonts: fonts:
- asset: fonts/materialdesignicons-webfont.ttf - asset: fonts/materialdesignicons-webfont-3-5-95.ttf
# fonts: # fonts:
# - family: Schyler # - family: Schyler
# fonts: # fonts:

File diff suppressed because one or more lines are too long