Compare commits

...

66 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
4b6dda5a9c version 0.4.4 2019-02-20 18:54:54 +02:00
4099fa0c83 WIP #302 fix SVG size 2019-02-20 18:50:58 +02:00
76057e8797 WIP #302 simple SVG support 2019-02-20 17:55:56 +02:00
538d3603dc Resolves #306 Improve camera connection 2019-02-20 16:39:57 +02:00
bc0e72ca52 version 0.4.3 2019-02-20 13:58:30 +02:00
f25a47beb2 Add camera stream reconnect on closing 2019-02-20 13:57:25 +02:00
cc3c6b0087 Resolves #307 Support different frame bounderies for MJPEG stream 2019-02-20 12:06:03 +02:00
6cf80c0bfd version 0.4.2 2019-02-19 19:22:40 +02:00
8ce9bdb7a5 Resolves #303, Resolves #304 Fix wrong camera stream url 2019-02-19 19:21:52 +02:00
31e50150b1 Resolves #263 Fix error when supported_features is null 2019-02-17 13:52:24 +02:00
75 changed files with 2851 additions and 1049 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
## Native Android client for Home Assistant
### With Lovelace UI support
Home Assistant Android client on Dart with Flutter.
Visit [www.vynn.co](https://www.vynn.co/ha-client) for more info.
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
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
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))
android {
compileSdkVersion 27
compileSdkVersion 28
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -43,7 +43,7 @@ android {
defaultConfig {
applicationId "com.keyboardcrumbs.haclient"
minSdkVersion 21
targetSdkVersion 27
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@ -70,7 +70,10 @@ flutter {
}
dependencies {
implementation 'com.google.firebase:firebase-core:16.0.8'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
apply plugin: 'com.google.gms.google-services'

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

View File

@ -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
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip

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

@ -19,8 +19,8 @@ class _EntityViewPageState extends State<EntityViewPage> {
void initState() {
super.initState();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
Logger.d("State change event handled by entity page: ${event.entityId}");
if (event.entityId == widget.entityId) {
Logger.d("State change event handled by entity page: ${event.entityId}");
setState(() {});
}
});

View File

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

View File

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

View File

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

View File

@ -4,16 +4,14 @@ class CameraEntity extends Entity {
static const SUPPORT_ON_OFF = 1;
CameraEntity(Map rawData) : super(rawData);
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportOnOff => ((attributes["supported_features"] &
bool get supportOnOff => ((supportedFeatures &
CameraEntity.SUPPORT_ON_OFF) ==
CameraEntity.SUPPORT_ON_OFF);
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return CameraControlsWidget(
url: '$homeAssistantWebHost/api/camera_proxy_stream/camera.demo_camera?token=${this.attributes['access_token']}',
);
return CameraStreamView();
}
}

View File

@ -23,44 +23,46 @@ class ClimateEntity extends Entity {
static const SUPPORT_AUX_HEAT = 2048;
static const SUPPORT_ON_OFF = 4096;
bool get supportTargetTemperature => ((attributes["supported_features"] &
ClimateEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportTargetTemperature => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
bool get supportTargetTemperatureHigh => ((attributes["supported_features"] &
bool get supportTargetTemperatureHigh => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
bool get supportTargetTemperatureLow => ((attributes["supported_features"] &
bool get supportTargetTemperatureLow => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
bool get supportTargetHumidity => ((attributes["supported_features"] &
bool get supportTargetHumidity => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
bool get supportTargetHumidityHigh => ((attributes["supported_features"] &
bool get supportTargetHumidityHigh => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
bool get supportTargetHumidityLow => ((attributes["supported_features"] &
bool get supportTargetHumidityLow => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
bool get supportFanMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_FAN_MODE) ==
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
ClimateEntity.SUPPORT_FAN_MODE);
bool get supportOperationMode => ((attributes["supported_features"] &
bool get supportOperationMode => ((supportedFeatures &
ClimateEntity.SUPPORT_OPERATION_MODE) ==
ClimateEntity.SUPPORT_OPERATION_MODE);
bool get supportHoldMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_HOLD_MODE) ==
((supportedFeatures & ClimateEntity.SUPPORT_HOLD_MODE) ==
ClimateEntity.SUPPORT_HOLD_MODE);
bool get supportSwingMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_SWING_MODE) ==
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
ClimateEntity.SUPPORT_SWING_MODE);
bool get supportAwayMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_AWAY_MODE) ==
((supportedFeatures & ClimateEntity.SUPPORT_AWAY_MODE) ==
ClimateEntity.SUPPORT_AWAY_MODE);
bool get supportAuxHeat =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_AUX_HEAT) ==
((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) ==
ClimateEntity.SUPPORT_AUX_HEAT);
bool get supportOnOff =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_ON_OFF) ==
((supportedFeatures & ClimateEntity.SUPPORT_ON_OFF) ==
ClimateEntity.SUPPORT_ON_OFF);
List<String> get operationList => attributes["operation_list"] != null
@ -88,11 +90,9 @@ class ClimateEntity extends Entity {
bool get isOff => state == EntityState.off;
bool get auxHeat => attributes['aux_heat'] == "on";
ClimateEntity(Map rawData) : super(rawData);
@override
void update(Map rawData) {
super.update(rawData);
void update(Map rawData, String webHost) {
super.update(rawData, webHost);
if (supportTargetTemperature) {
historyConfig.numericAttributesToShow.add("temperature");
}

View File

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

View File

@ -11,29 +11,31 @@ class CoverEntity extends Entity {
static const SUPPORT_STOP_TILT = 64;
static const SUPPORT_SET_TILT_POSITION = 128;
bool get supportOpen => ((attributes["supported_features"] &
CoverEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportOpen => ((supportedFeatures &
CoverEntity.SUPPORT_OPEN) ==
CoverEntity.SUPPORT_OPEN);
bool get supportClose => ((attributes["supported_features"] &
bool get supportClose => ((supportedFeatures &
CoverEntity.SUPPORT_CLOSE) ==
CoverEntity.SUPPORT_CLOSE);
bool get supportSetPosition => ((attributes["supported_features"] &
bool get supportSetPosition => ((supportedFeatures &
CoverEntity.SUPPORT_SET_POSITION) ==
CoverEntity.SUPPORT_SET_POSITION);
bool get supportStop => ((attributes["supported_features"] &
bool get supportStop => ((supportedFeatures &
CoverEntity.SUPPORT_STOP) ==
CoverEntity.SUPPORT_STOP);
bool get supportOpenTilt => ((attributes["supported_features"] &
bool get supportOpenTilt => ((supportedFeatures &
CoverEntity.SUPPORT_OPEN_TILT) ==
CoverEntity.SUPPORT_OPEN_TILT);
bool get supportCloseTilt => ((attributes["supported_features"] &
bool get supportCloseTilt => ((supportedFeatures &
CoverEntity.SUPPORT_CLOSE_TILT) ==
CoverEntity.SUPPORT_CLOSE_TILT);
bool get supportStopTilt => ((attributes["supported_features"] &
bool get supportStopTilt => ((supportedFeatures &
CoverEntity.SUPPORT_STOP_TILT) ==
CoverEntity.SUPPORT_STOP_TILT);
bool get supportSetTiltPosition => ((attributes["supported_features"] &
bool get supportSetTiltPosition => ((supportedFeatures &
CoverEntity.SUPPORT_SET_TILT_POSITION) ==
CoverEntity.SUPPORT_SET_TILT_POSITION);
@ -45,8 +47,6 @@ class CoverEntity extends Entity {
bool get canTiltBeOpened => currentTiltPosition < 100;
bool get canTiltBeClosed => currentTiltPosition > 0;
CoverEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return CoverStateWidget();

View File

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

View File

@ -1,5 +1,14 @@
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 {
static List badgeDomains = [
@ -64,12 +73,13 @@ class Entity {
Map attributes;
String domain;
String entityId;
String entityPicture;
String state;
String displayState;
DateTime _lastUpdated;
int statelessType = 0;
List<Entity> childEntities = [];
List<String> attributesToShow = ["all"];
String deviceClass;
EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.simple
@ -85,18 +95,60 @@ class Entity {
bool get isBadge => Entity.badgeDomains.contains(domain);
String get icon => attributes["icon"] ?? "";
bool get isOn => state == EntityState.on;
String get entityPicture => attributes["entity_picture"];
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
List get childEntityIds => attributes["entity_id"] ?? [];
String get lastUpdated => _getLastUpdatedFormatted();
bool get isHidden => attributes["hidden"] ?? false;
double get doubleState => double.tryParse(state) ?? 0.0;
int get supportedFeatures => attributes["supported_features"] ?? 0;
Entity(Map rawData) {
update(rawData);
String _getEntityPictureUrl(String webHost) {
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"] ?? {};
domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"];
@ -104,6 +156,7 @@ class Entity {
state = rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
entityPicture = _getEntityPictureUrl(webHost);
}
double _getDoubleAttributeValue(String attributeName) {

View File

@ -4,6 +4,7 @@ class EntityWrapper {
String displayName;
String icon;
String entityPicture;
EntityUIAction uiAction;
Entity entity;
@ -14,10 +15,15 @@ class EntityWrapper {
String displayName,
this.uiAction
}) {
this.icon = icon ?? entity.icon;
this.displayName = displayName ?? entity.displayName;
if (this.uiAction == null) {
this.uiAction = EntityUIAction();
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
this.icon = icon ?? entity.icon;
if (icon == null) {
entityPicture = entity.entityPicture;
}
this.displayName = displayName ?? entity.displayName;
if (uiAction == null) {
uiAction = EntityUIAction();
}
}
}
@ -49,6 +55,16 @@ class EntityWrapper {
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: {
break;
}
@ -79,6 +95,16 @@ class EntityWrapper {
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: {
break;
}

View File

@ -6,15 +6,15 @@ class FanEntity extends Entity {
static const SUPPORT_OSCILLATE = 2;
static const SUPPORT_DIRECTION = 4;
FanEntity(Map rawData) : super(rawData);
FanEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportSetSpeed => ((attributes["supported_features"] &
bool get supportSetSpeed => ((supportedFeatures &
FanEntity.SUPPORT_SET_SPEED) ==
FanEntity.SUPPORT_SET_SPEED);
bool get supportOscillate => ((attributes["supported_features"] &
bool get supportOscillate => ((supportedFeatures &
FanEntity.SUPPORT_OSCILLATE) ==
FanEntity.SUPPORT_OSCILLATE);
bool get supportDirection => ((attributes["supported_features"] &
bool get supportDirection => ((supportedFeatures &
FanEntity.SUPPORT_DIRECTION) ==
FanEntity.SUPPORT_DIRECTION);

View File

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

View File

@ -10,46 +10,50 @@ class LightEntity extends Entity {
static const SUPPORT_TRANSITION = 32;
static const SUPPORT_WHITE_VALUE = 128;
bool get supportBrightness => ((attributes["supported_features"] &
bool get supportBrightness => ((supportedFeatures &
LightEntity.SUPPORT_BRIGHTNESS) ==
LightEntity.SUPPORT_BRIGHTNESS);
bool get supportColorTemp => ((attributes["supported_features"] &
bool get supportColorTemp => ((supportedFeatures &
LightEntity.SUPPORT_COLOR_TEMP) ==
LightEntity.SUPPORT_COLOR_TEMP);
bool get supportEffect => ((attributes["supported_features"] &
bool get supportEffect => ((supportedFeatures &
LightEntity.SUPPORT_EFFECT) ==
LightEntity.SUPPORT_EFFECT);
bool get supportFlash => ((attributes["supported_features"] &
bool get supportFlash => ((supportedFeatures &
LightEntity.SUPPORT_FLASH) ==
LightEntity.SUPPORT_FLASH);
bool get supportColor => ((attributes["supported_features"] &
bool get supportColor => ((supportedFeatures &
LightEntity.SUPPORT_COLOR) ==
LightEntity.SUPPORT_COLOR);
bool get supportTransition => ((attributes["supported_features"] &
bool get supportTransition => ((supportedFeatures &
LightEntity.SUPPORT_TRANSITION) ==
LightEntity.SUPPORT_TRANSITION);
bool get supportWhiteValue => ((attributes["supported_features"] &
bool get supportWhiteValue => ((supportedFeatures &
LightEntity.SUPPORT_WHITE_VALUE) ==
LightEntity.SUPPORT_WHITE_VALUE);
int get brightness => _getIntAttributeValue("brightness");
int get whiteValue => _getIntAttributeValue("white_value");
String get effect => attributes["effect"];
int get colorTemp => _getIntAttributeValue("color_temp");
double get maxMireds => _getDoubleAttributeValue("max_mireds");
double get minMireds => _getDoubleAttributeValue("min_mireds");
HSVColor get color => _getColor();
bool get isAdditionalControls => ((attributes["supported_features"] != null) && (attributes["supported_features"] != 0));
bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0));
List<String> get effectList => getStringListAttributeValue("effect_list");
LightEntity(Map rawData) : super(rawData);
LightEntity(Map rawData, String webHost) : super(rawData, webHost);
HSVColor _getColor() {
List hs = attributes["hs_color"];
List rgb = attributes["rgb_color"];
try {
if ((hs != null) && (hs.length > 0)) {
if (hs != null && hs.isNotEmpty) {
double sat = hs[1]/100;
String ssat = sat.toStringAsFixed(2);
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 {
return null;
}

View File

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

View File

@ -20,55 +20,55 @@ class MediaPlayerEntity extends Entity {
static const SUPPORT_SHUFFLE_SET = 32768;
static const SUPPORT_SELECT_SOUND_MODE = 65536;
MediaPlayerEntity(Map rawData) : super(rawData);
MediaPlayerEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportPause => ((attributes["supported_features"] &
bool get supportPause => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_PAUSE) ==
MediaPlayerEntity.SUPPORT_PAUSE);
bool get supportSeek => ((attributes["supported_features"] &
bool get supportSeek => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_SEEK) ==
MediaPlayerEntity.SUPPORT_SEEK);
bool get supportVolumeSet => ((attributes["supported_features"] &
bool get supportVolumeSet => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_VOLUME_SET) ==
MediaPlayerEntity.SUPPORT_VOLUME_SET);
bool get supportVolumeMute => ((attributes["supported_features"] &
bool get supportVolumeMute => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_VOLUME_MUTE) ==
MediaPlayerEntity.SUPPORT_VOLUME_MUTE);
bool get supportPreviousTrack => ((attributes["supported_features"] &
bool get supportPreviousTrack => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK) ==
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK);
bool get supportNextTrack => ((attributes["supported_features"] &
bool get supportNextTrack => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_NEXT_TRACK) ==
MediaPlayerEntity.SUPPORT_NEXT_TRACK);
bool get supportTurnOn => ((attributes["supported_features"] &
bool get supportTurnOn => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_TURN_ON) ==
MediaPlayerEntity.SUPPORT_TURN_ON);
bool get supportTurnOff => ((attributes["supported_features"] &
bool get supportTurnOff => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_TURN_OFF) ==
MediaPlayerEntity.SUPPORT_TURN_OFF);
bool get supportPlayMedia => ((attributes["supported_features"] &
bool get supportPlayMedia => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_PLAY_MEDIA) ==
MediaPlayerEntity.SUPPORT_PLAY_MEDIA);
bool get supportVolumeStep => ((attributes["supported_features"] &
bool get supportVolumeStep => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_VOLUME_STEP) ==
MediaPlayerEntity.SUPPORT_VOLUME_STEP);
bool get supportSelectSource => ((attributes["supported_features"] &
bool get supportSelectSource => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_SELECT_SOURCE) ==
MediaPlayerEntity.SUPPORT_SELECT_SOURCE);
bool get supportStop => ((attributes["supported_features"] &
bool get supportStop => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_STOP) ==
MediaPlayerEntity.SUPPORT_STOP);
bool get supportClearPlaylist => ((attributes["supported_features"] &
bool get supportClearPlaylist => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST) ==
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST);
bool get supportPlay => ((attributes["supported_features"] &
bool get supportPlay => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_PLAY) ==
MediaPlayerEntity.SUPPORT_PLAY);
bool get supportShuffleSet => ((attributes["supported_features"] &
bool get supportShuffleSet => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_SHUFFLE_SET) ==
MediaPlayerEntity.SUPPORT_SHUFFLE_SET);
bool get supportSelectSoundMode => ((attributes["supported_features"] &
bool get supportSelectSoundMode => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE) ==
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,12 @@ class ButtonEntityContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
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(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
@ -19,11 +24,11 @@ class ButtonEntityContainer extends StatelessWidget {
FractionallySizedBox(
widthFactor: 0.4,
child: FittedBox(
fit: BoxFit.fitHeight,
child: EntityIcon(
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
iconSize: Sizes.iconSize,
)
fit: BoxFit.fitHeight,
child: EntityIcon(
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
size: Sizes.iconSize,
)
),
),
_buildName()

View File

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

View File

@ -0,0 +1,177 @@
part of '../../main.dart';
class CameraStreamView extends StatefulWidget {
CameraStreamView({Key key}) : super(key: key);
@override
_CameraStreamViewState createState() => _CameraStreamViewState();
}
class _CameraStreamViewState extends State<CameraStreamView> {
@override
void initState() {
super.initState();
}
CameraEntity _entity;
String _webHost;
http.Client client;
http.StreamedResponse response;
List<int> binaryImage = [];
bool timeToStop = false;
Completer streamCompleter;
bool started = false;
bool useSVG = false;
void _connect() async {
started = true;
timeToStop = false;
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
http.Request request = new http.Request("GET", Uri.parse(streamUrl)); // create get request
Logger.d("[Sending] ==> $streamUrl");
response = await client.send(request);
Logger.d("[Received] <== ${response.headers}");
String frameBoundary = response.headers['content-type'].split('boundary=')[1];
final int frameBoundarySize = frameBoundary.length;
List<int> primaryBuffer=[];
int imageSizeStart = 59;
int imageSizeEnd = 0;
int imageStart = 0;
int imageSize = 0;
String strBuffer = "";
String contentType = "";
streamCompleter = Completer();
response.stream.transform(
StreamTransformer.fromHandlers(
handleData: (data, sink) {
primaryBuffer.addAll(data);
imageStart = 0;
imageSizeEnd = 0;
if (primaryBuffer.length >= imageSizeStart + 10) {
contentType = utf8.decode(
primaryBuffer.sublist(frameBoundarySize+16, imageSizeStart + 10), allowMalformed: true).split("\r\n")[0];
useSVG = contentType == "image/svg+xml";
imageSizeStart = frameBoundarySize + 16 + contentType.length + 18;
for (int i = imageSizeStart; i < primaryBuffer.length - 4; i++) {
strBuffer = utf8.decode(
primaryBuffer.sublist(i, i + 4), allowMalformed: true);
if (strBuffer == "\r\n\r\n") {
imageSizeEnd = i;
imageStart = i + 4;
break;
}
}
if (imageSizeEnd > 0) {
imageSize = int.tryParse(utf8.decode(
primaryBuffer.sublist(imageSizeStart, imageSizeEnd),
allowMalformed: true));
//Logger.d("content-length: $imageSize");
if (imageSize != null &&
primaryBuffer.length >= imageStart + imageSize + 2) {
sink.add(
primaryBuffer.sublist(
imageStart, imageStart + imageSize));
primaryBuffer.removeRange(0, imageStart + imageSize + 2);
}
}
}
if (timeToStop) {
sink?.close();
streamCompleter.complete();
}
},
handleError: (error, stack, sink) {
Logger.e("Error parsing MJPEG stream: $error");
},
handleDone: (sink) {
Logger.d("Camera stream finished. Reconnecting...");
sink?.close();
streamCompleter?.complete();
_reconnect();
},
)
).listen((d) {
if (!timeToStop) {
setState(() {
binaryImage = d;
});
}
});
}
void _reconnect() {
disconnect().then((_){
_connect();
});
}
Future disconnect() {
Completer disconF = Completer();
timeToStop = true;
if (streamCompleter != null && !streamCompleter.isCompleted) {
streamCompleter.future.then((_) {
client?.close();
disconF.complete();
});
} else {
client?.close();
disconF.complete();
}
return disconF.future;
}
@override
Widget build(BuildContext context) {
if (!started) {
_entity = EntityModel
.of(context)
.entityWrapper
.entity;
_webHost = HomeAssistantModel.of(context).homeAssistant.connection.httpWebHost;
_connect();
}
if (binaryImage.isEmpty) {
return Column(
children: <Widget>[
Container(
padding: const EdgeInsets.all(20.0),
child: const CircularProgressIndicator()
)
],
);
} else {
if (useSVG) {
return Column(
children: <Widget>[
SvgPicture.memory(
Uint8List.fromList(binaryImage),
placeholderBuilder: (BuildContext context) =>
new Container(
padding: const EdgeInsets.all(20.0),
child: const CircularProgressIndicator()
),
)
],
);
} else {
return Column(
children: <Widget>[
Image.memory(
Uint8List.fromList(binaryImage), gaplessPlayback: true),
],
);
}
}
}
@override
void dispose() {
disconnect();
super.dispose();
}
}

View File

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

View File

@ -1,115 +0,0 @@
part of '../../main.dart';
class CameraControlsWidget extends StatefulWidget {
final String url;
CameraControlsWidget({Key key, @required this.url}) : super(key: key);
@override
_CameraControlsWidgetState createState() => _CameraControlsWidgetState();
}
class _CameraControlsWidgetState extends State<CameraControlsWidget> {
@override
void initState() {
super.initState();
_getData();
}
http.Client client;
http.StreamedResponse response;
List<int> binaryImage = [];
String cameraState = "Connecting...";
bool timeToStop = false;
Completer streamCompleter;
void _getData() async {
client = new http.Client(); // create a client to make api calls
http.Request request = new http.Request("GET", Uri.parse(widget.url)); // create get request
Logger.d("[Sending] ==> ${widget.url}");
response = await client.send(request);
setState(() {
cameraState = "Starting...";
});
Logger.d("[Received] <== ${response.headers}");
List<int> primaryBuffer=[];
final int imageSizeStart = 59;
int imageSizeEnd = 0;
int imageStart = 0;
int imageSize = 0;
String strBuffer = "";
streamCompleter = Completer();
response.stream.transform(
StreamTransformer.fromHandlers(
handleData: (data, sink) {
primaryBuffer.addAll(data);
if (primaryBuffer.length >= 66) {
for (int i = imageSizeStart; i < primaryBuffer.length - 4; i++) {
strBuffer = utf8.decode(
primaryBuffer.sublist(i, i + 4), allowMalformed: true);
if (strBuffer == "\r\n\r\n") {
imageSizeEnd = i;
imageStart = i + 4;
break;
}
}
imageSize = int.tryParse(utf8.decode(
primaryBuffer.sublist(imageSizeStart, imageSizeEnd),
allowMalformed: true));
if (imageSize != null && primaryBuffer.length >= imageStart + imageSize + 2) {
sink.add(
primaryBuffer.sublist(imageStart, imageStart + imageSize));
primaryBuffer.removeRange(0, imageStart + imageSize + 2);
}
}
if (timeToStop) {
sink?.close();
streamCompleter.complete();
}
},
handleError: (error, stack, sink) {
Logger.e("Error parsing MJPEG stream: $error");
},
handleDone: (sink) {
sink?.close();
},
)
).listen((d) {
setState(() {
binaryImage = d;
});
});
}
@override
Widget build(BuildContext context) {
if (binaryImage.isEmpty) {
return Column(
children: <Widget>[
Text("$cameraState")
],
);
} else {
return Column(
children: <Widget>[
Image.memory(Uint8List.fromList(binaryImage), gaplessPlayback: true),
],
);
}
}
@override
void dispose() {
super.dispose();
timeToStop = true;
if (streamCompleter != null && !streamCompleter.isCompleted) {
streamCompleter.future.then((_) {
client?.close();
});
} else {
client?.close();
}
}
}

View File

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

View File

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

View File

@ -10,6 +10,7 @@ class LightControlsWidget extends StatefulWidget {
class _LightControlsWidgetState extends State<LightControlsWidget> {
int _tmpBrightness;
int _tmpWhiteValue;
int _tmpColorTemp = 0;
HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0);
bool _changedHere = false;
@ -17,6 +18,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
void _resetState(LightEntity entity) {
_tmpBrightness = entity.brightness ?? 0;
_tmpWhiteValue = entity.whiteValue ?? 0;
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
_tmpColor = entity.color ?? _tmpColor;
_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) {
setState(() {
_tmpColorTemp = value.round();
@ -84,6 +97,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
_buildBrightnessControl(entity),
_buildWhiteValueControl(entity),
_buildColorTempControl(entity),
_buildColorControl(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) {
if (entity.supportColorTemp) {
return UniversalSlider(
@ -136,30 +171,45 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
Widget _buildColorControl(LightEntity entity) {
if (entity.supportColor) {
return LightColorPicker(
color: _tmpColor,
onColorSelected: (color) => _setColor(entity, color),
HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor;
return Column(
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 {
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) {
if ((entity.supportEffect) && (entity.effectList != null)) {
return ModeSelectorWidget(

View File

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

View File

@ -11,6 +11,25 @@ class DefaultEntityContainer extends StatelessWidget {
@override
Widget build(BuildContext 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(
onLongPress: () {
if (entityModel.handleTap) {
@ -30,7 +49,9 @@ class DefaultEntityContainer extends StatelessWidget {
Flexible(
fit: FlexFit.tight,
flex: 3,
child: EntityName(),
child: EntityName(
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
),
),
state
],

View File

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

View File

@ -3,20 +3,71 @@ part of '../main.dart';
class EntityIcon extends StatelessWidget {
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(
Sizes.leftWidgetPadding, 0.0, 12.0, 0.0)}) : super(key: key);
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(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
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return Padding(
padding: padding,
child: MaterialDesignIcons.createIconWidgetFromEntityData(
child: buildIcon(
entityWrapper,
iconSize,
EntityColor.stateColor(entityWrapper.entity.state)
color ?? EntityColor.stateColor(entityWrapper.entity.state)
),
);
}

View File

@ -14,6 +14,10 @@ class EntityName extends StatelessWidget {
@override
Widget build(BuildContext context) {
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(
padding: padding,
child: Text(
@ -21,7 +25,7 @@ class EntityName extends StatelessWidget {
overflow: textOverflow,
softWrap: wordsWrap,
maxLines: maxLines,
style: TextStyle(fontSize: fontSize),
style: textStyle,
textAlign: textAlign,
),
);

View File

@ -22,6 +22,12 @@ class GlanceEntityContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
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 = [];
if (!nameInTheBottom) {
if (showName) {
@ -35,7 +41,7 @@ class GlanceEntityContainer extends StatelessWidget {
result.add(
EntityIcon(
padding: EdgeInsets.all(0.0),
iconSize: iconSize,
size: iconSize,
)
);
if (!nameInTheBottom) {

View File

@ -32,6 +32,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
List _history;
bool _needToUpdateHistory;
DateTime _historyLastUpdated;
bool _disposed = false;
@override
void initState() {
@ -46,17 +47,21 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
}
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
_historyLastUpdated = now;
ha.getHistory(entityId).then((history){
setState(() {
_history = history.isNotEmpty ? history[0] : [];
_needToUpdateHistory = false;
});
ha.connection.getHistory(entityId).then((history){
if (!_disposed) {
setState(() {
_history = history.isNotEmpty ? history[0] : [];
_needToUpdateHistory = false;
});
}
}).catchError((e) {
Logger.e("Error loading $entityId history: $e");
setState(() {
_history = [];
_needToUpdateHistory = false;
});
if (!_disposed) {
setState(() {
_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) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-up"),
MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-up"),
size: Sizes.iconSize,
),
onPressed: entity.canBeOpened ? () => _open(entity) : null));
@ -36,7 +36,7 @@ class CoverStateWidget extends StatelessWidget {
if (entity.supportStop) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
MaterialDesignIcons.getIconDataFromIconName("mdi:stop"),
size: Sizes.iconSize,
),
onPressed: () => _stop(entity)));
@ -48,7 +48,7 @@ class CoverStateWidget extends StatelessWidget {
if (entity.supportClose) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-down"),
MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-down"),
size: Sizes.iconSize,
),
onPressed: entity.canBeClosed ? () => _close(entity) : null));

View File

@ -2,6 +2,10 @@ part of '../../main.dart';
class LockStateWidget extends StatelessWidget {
final bool assumedState;
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
void _lock(Entity entity) {
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null));
}
@ -14,19 +18,49 @@ class LockStateWidget extends StatelessWidget {
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final LockEntity entity = entityModel.entityWrapper.entity;
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),
if (assumedState) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(
height: 34.0,
child: FlatButton(
onPressed: () => _unlock(entity),
child: Text("UNLOCK",
textAlign: TextAlign.right,
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 EdgeInsetsGeometry padding;
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
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
String state = entityModel.entityWrapper.entity.displayState ?? "";
state = state.replaceAll("\n", "").replaceAll("\t", " ").trim();
String state;
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(" ")){
state = state.replaceAll(" ", " ");
}
@ -25,9 +37,7 @@ class SimpleEntityState extends StatelessWidget {
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
)
style: textStyle
)
);
if (expanded) {

View File

@ -71,13 +71,13 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
children: <Widget>[
IconButton(
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,
iconSize: Sizes.iconSize,
),
IconButton(
onPressed: () => _setNewState(true, entity),
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash")),
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash")),
color: newState == EntityState.on ? Colors.blue : Colors.black,
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';
class HomeAssistant {
String _webSocketAPIEndpoint;
String _password;
final Connection connection = Connection();
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;
HomeAssistantUI ui;
Map _instanceConfig = {};
String _userName;
String hostname;
HSVColor savedColor;
Map _rawLovelaceData;
Completer _fetchCompleter;
Completer _statesCompleter;
Completer _servicesCompleter;
Completer _lovelaceCompleter;
Completer _configCompleter;
Completer _connectionCompleter;
Completer _userInfoCompleter;
Timer _connectionTimer;
Timer _fetchTimer;
bool autoReconnect = false;
List<Panel> panels = [];
StreamSubscription _socketSubscription;
int messageExpirationTime = 30; //seconds
Duration fetchTimeout = Duration(seconds: 30);
Duration connectTimeout = Duration(seconds: 15);
String get locationName {
if (_useLovelace) {
@ -48,117 +32,39 @@ class HomeAssistant {
}
String get userName => _userName ?? locationName;
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;
HomeAssistant() {
entities = EntityCollection();
_messageQueue = SendMessageQueue(messageExpirationTime);
HomeAssistant();
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) {
_webSocketAPIEndpoint = url;
_password = password;
_useLovelace = useLovelace;
Logger.d( "Use lovelace is $_useLovelace");
}
Completer _fetchCompleter;
Future fetch() {
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
Logger.w("Previous fetch is not complited");
} else {
_fetchCompleter = new Completer();
_fetchTimer = Timer(fetchTimeout, () {
Logger.e( "Data fetching timeout");
disconnect().then((_) {
_completeFetching({
"errorCode": 9,
"errorMessage": "Couldn't get data from server"
});
});
});
_connection().then((r) {
_getData();
}).catchError((e) {
_completeFetching(e);
});
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
Logger.w("Previous data fetch is not completed yet");
return _fetchCompleter.future;
}
return _fetchCompleter.future;
}
disconnect() async {
if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) {
await _hassioChannel.sink.close().timeout(Duration(seconds: 3),
onTimeout: () => Logger.d( "Socket sink closed")
);
await _socketSubscription.cancel();
_hassioChannel = null;
}
}
Future _connection() {
if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
Logger.d("Previous connection is not complited");
} else {
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
_connectionCompleter = new Completer();
autoReconnect = false;
disconnect().then((_){
Logger.d( "Socket connecting...");
_connectionTimer = Timer(connectTimeout, () {
Logger.e( "Socket connection timeout");
_handleSocketError(null);
});
if (_socketSubscription != null) {
_socketSubscription.cancel();
}
_hassioChannel = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 30));
_socketSubscription = _hassioChannel.stream.listen(
(message) => _handleMessage(message),
cancelOnError: true,
onDone: () => _handleSocketClose(),
onError: (e) => _handleSocketError(e)
);
});
} else {
_completeConnecting(null);
}
}
return _connectionCompleter.future;
}
void _handleSocketClose() {
Logger.d("Socket disconnected. Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
}
}
void _handleSocketError(e) {
Logger.e("Socket stream Error: $e");
Logger.d("Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
} else {
disconnect().then((_) {
_completeConnecting({
"errorCode": 1,
"errorMessage": "Couldn't connect to Home Assistant. Check network connection or connection settings."
});
});
}
}
void _reconnect() {
disconnect().then((_) {
_connection().catchError((e){
_completeConnecting(e);
});
});
}
_getData() async {
_fetchCompleter = Completer();
List<Future> futures = [];
futures.add(_getStates());
if (_useLovelace) {
@ -167,199 +73,77 @@ class HomeAssistant {
futures.add(_getConfig());
futures.add(_getServices());
futures.add(_getUserInfo());
try {
await Future.wait(futures);
futures.add(_getPanels());
Future.wait(futures).then((_) {
_createUI();
_completeFetching(null);
} catch (error) {
_completeFetching(error);
}
}
void _completeFetching(error) {
_fetchTimer.cancel();
_completeConnecting(error);
if (!_fetchCompleter.isCompleted) {
if (error != null) {
_fetchCompleter.completeError(error);
} else {
autoReconnect = true;
Logger.d( "Fetch complete successful");
_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);
_fetchCompleter.complete();
}).catchError((e) {
_fetchCompleter.completeError(e);
});
return sendCompleter.future;
return _fetchCompleter.future;
}
Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
_incrementMessageId();
String message = "";
if (entityId != null) {
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
if (additionalParams != null) {
additionalParams.forEach((name, value) {
if ((value is double) || (value is int) || (value is List)) {
message += ', "$name" : $value';
} else {
message += ', "$name" : "$value"';
}
});
}
message += '}}';
} else {
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service"';
if (additionalParams != null && additionalParams.isNotEmpty) {
message += ', "service_data": {';
bool first = true;
additionalParams.forEach((name, value) {
if (!first) {
message += ', ';
}
if ((value is double) || (value is int) || (value is List)) {
message += '"$name" : $value';
} else {
message += '"$name" : "$value"';
}
first = false;
});
Future logout() async {
Logger.d("Logging out...");
await connection.logout().then((_) {
ui?.clear();
entities?.clear();
});
}
message += '}';
}
message += '}';
}
return _sendMessageRaw(message, true);
Future _getConfig() async {
await connection.sendSocketMessage(type: "get_config").then((data) {
_instanceConfig = Map.from(data);
}).catchError((e) {
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) {
@ -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() {
Logger.d("--Title: ${_rawLovelaceData["title"]}");
ui.title = _rawLovelaceData["title"];
@ -467,9 +218,51 @@ class HomeAssistant {
if (rawEntity is String) {
if (entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
}
} 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"]);
card.entities.add(
EntityWrapper(
@ -479,6 +272,8 @@ class HomeAssistant {
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
}
}
});
@ -493,6 +288,8 @@ class HomeAssistant {
displayName: rawCard["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
}
} else {
if (entities.isExist(en["entity"])) {
@ -503,6 +300,8 @@ class HomeAssistant {
displayName: en["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
}
}
}
@ -514,15 +313,6 @@ class HomeAssistant {
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() {
ui = HomeAssistantUI();
if ((_useLovelace) && (_rawLovelaceData != null)) {
@ -559,31 +349,12 @@ class HomeAssistant {
}
}
Widget buildViews(BuildContext context, bool lovelace) {
return ui.build(context);
}
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 [];
}
Widget buildViews(BuildContext context, TabController tabController) {
return ui.build(context, tabController);
}
}
/*
class SendMessageQueue {
int _messageTimeout;
List<HAMessage> _queue = [];
@ -622,4 +393,4 @@ class HAMessage {
bool isExpired() {
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
}
}
}*/

View File

@ -8,17 +8,23 @@ import 'package:web_socket_channel/io.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/widgets.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:date_format/date_format.dart';
import 'package:http/http.dart' as http;
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:progress_indicators/progress_indicators.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_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/entity.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/button_entity.class.dart';
part 'entity_class/text_entity.class.dart';
@ -39,6 +45,7 @@ part 'entity_class/alarm_control_panel.class.dart';
part 'entity_widgets/common/badge.dart';
part 'entity_widgets/model_widgets.dart';
part 'entity_widgets/default_entity_container.dart';
part 'entity_widgets/missed_entity.dart';
part 'entity_widgets/glance_entity_container.dart';
part 'entity_widgets/button_entity_container.dart';
part 'entity_widgets/common/entity_attributes_list.dart';
@ -50,6 +57,7 @@ part 'entity_widgets/common/mode_selector.dart';
part 'entity_widgets/common/universal_slider.dart';
part 'entity_widgets/common/flat_service_button.dart';
part 'entity_widgets/common/light_color_picker.dart';
part 'entity_widgets/common/camera_stream_view.dart';
part 'entity_widgets/entity_colors.class.dart';
part 'entity_widgets/entity_page_container.dart';
part 'entity_widgets/history_chart/entity_history.dart';
@ -63,6 +71,7 @@ part 'entity_widgets/controls/slider_controls.dart';
part 'entity_widgets/state/text_input_state.dart';
part 'entity_widgets/state/select_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/cover_state.dart';
part 'entity_widgets/state/date_time_state.dart';
@ -73,30 +82,30 @@ part 'entity_widgets/controls/light_controls.dart';
part 'entity_widgets/controls/media_player_widgets.dart';
part 'entity_widgets/controls/fan_controls.dart';
part 'entity_widgets/controls/alarm_control_panel_controls.dart';
part 'entity_widgets/controls/camera_controls.dart';
part 'settings.page.dart';
part 'configuration.page.dart';
part 'panel.page.dart';
part 'home_assistant.class.dart';
part 'log.page.dart';
part 'entity.page.dart';
part 'utils.class.dart';
part 'mdi.class.dart';
part 'entity_collection.class.dart';
part 'auth_manager.class.dart';
part 'connection.class.dart';
part 'ui_class/ui.dart';
part 'ui_class/view.class.dart';
part 'ui_class/card.class.dart';
part 'ui_class/sizes_class.dart';
part 'ui_class/panel_class.dart';
part 'ui_widgets/view.dart';
part 'ui_widgets/card_widget.dart';
part 'ui_widgets/card_header_widget.dart';
part 'ui_widgets/config_panel_widget.dart';
EventBus eventBus = new EventBus();
const String appName = "HA Client";
const appVersion = "0.4.0";
const appBuild = "91";
String homeAssistantWebHost;
const appVersion = "0.6.0-alpha1";
void main() {
FlutterError.onError = (errorDetails) {
@ -118,6 +127,8 @@ void main() {
}
class HAClientApp extends StatelessWidget {
final HomeAssistant homeAssistant = HomeAssistant();
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
@ -128,9 +139,9 @@ class HAClientApp extends StatelessWidget {
),
initialRoute: "/",
routes: {
"/": (context) => MainPage(title: 'HA Client'),
"/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
"/configuration": (context) => ConfigurationPage(title: "Configuration"),
"/configuration": (context) => PanelPage(title: "Configuration"),
"/log-view": (context) => LogViewPage(title: "Log")
},
);
@ -138,81 +149,84 @@ class HAClientApp extends StatelessWidget {
}
class MainPage extends StatefulWidget {
MainPage({Key key, this.title}) : super(key: key);
MainPage({Key key, this.title, this.homeAssistant}) : super(key: key);
final String title;
final HomeAssistant homeAssistant;
@override
_MainPageState createState() => new _MainPageState();
}
class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
HomeAssistant _homeAssistant;
//Map _instanceConfig;
String _webSocketApiEndpoint;
String _password;
//int _uiViewsCount = 0;
String _instanceHost;
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
StreamSubscription _stateSubscription;
StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription;
StreamSubscription _showErrorSubscription;
bool _settingsLoaded = false;
bool _accountMenuExpanded = false;
bool _useLovelaceUI;
StreamSubscription _startAuthSubscription;
StreamSubscription _reloadUISubscription;
int _previousViewCount;
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
@override
void initState() {
super.initState();
_settingsLoaded = false;
//widget.homeAssistant = HomeAssistant();
//_settingsLoaded = false;
WidgetsBinding.instance.addObserver(this);
Logger.d("<!!!> Creating new HomeAssistant instance");
_homeAssistant = HomeAssistant();
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
Logger.d("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) {
_homeAssistant.disconnect().then((_){
_initialLoad();
});
_reLoad();
}
});
_initialLoad();
}
void _initialLoad() {
_loadConnectionSettings().then((_){
_subscribe();
_refreshData();
}, onError: (_) {
_showErrorBottomBar(message: _, errorCode: 5);
_showInfoBottomBar(progress: true,);
_subscribe();
widget.homeAssistant.init().then((_){
_fetchData();
}, 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
void didChangeAppLifecycleState(AppLifecycleState state) {
Logger.d("$state");
if (state == AppLifecycleState.resumed && _settingsLoaded) {
_refreshData();
}
}
_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;
if (state == AppLifecycleState.resumed) {
_reLoad();
}
}
@ -221,12 +235,17 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) {
Logger.d("New entity. Need to rebuild UI");
_refreshData();
_reLoad();
} else {
setState(() {});
}
});
}
if (_reloadUISubscription == null) {
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
_reLoad();
});
}
if (_serviceCallSubscription == null) {
_serviceCallSubscription =
eventBus.on<ServiceCallEvent>().listen((event) {
@ -247,18 +266,51 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_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 {
_homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI);
_hideBottomBar();
_showInfoBottomBar(progress: true,);
await _homeAssistant.fetch().then((result) {
_hideBottomBar();
}).catchError((e) {
_setErrorState(e);
});
eventBus.fire(RefreshDataFinishedEvent());
void _showOAuth() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebviewScaffold(
url: "${widget.homeAssistant.connection.oauthUrl}",
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.help),
onPressed: () => HAUtils.launchURLInCustomTab(context, "http://ha-client.homemade.systems/docs#authentication")
),
title: new Text("Login to your Home Assistant"),
),
),
)
);
}
_setErrorState(e) {
@ -266,7 +318,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
Logger.e(e.toString());
Logger.e("${e.stackTrace}");
_showErrorBottomBar(
message: "There was some error",
message: "Unknown error",
errorCode: 13
);
} 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(
message: "Calling $domain.$service",
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) {
Navigator.push(
context,
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> result = [];
if (_homeAssistant.ui.views.isNotEmpty) {
_homeAssistant.ui.views.forEach((HAView view) {
if (widget.homeAssistant.ui.views.isNotEmpty) {
widget.homeAssistant.ui.views.forEach((HAView view) {
result.add(view.buildTab());
});
}
@ -310,16 +362,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
List<Widget> menuItems = [];
menuItems.add(
UserAccountsDrawerHeader(
accountName: Text(_homeAssistant.userName),
accountEmail: Text(_instanceHost ?? "Not configured"),
onDetailsPressed: () {
accountName: Text(widget.homeAssistant.userName),
accountEmail: Text(widget.homeAssistant.hostname ?? "Not configured"),
/*onDetailsPressed: () {
setState(() {
_accountMenuExpanded = !_accountMenuExpanded;
});
},
},*/
currentAccountPicture: CircleAvatar(
child: Text(
_homeAssistant.userAvatarText,
widget.homeAssistant.userAvatarText,
style: TextStyle(
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([
ListTile(
leading: Icon(Icons.settings),
title: Text("Settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/connection-settings');
},
),
Divider(),
]);
} else {
menuItems.addAll([
new ListTile(
leading: Icon(Icons.settings),
title: Text("Configuration"),
ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
title: Text("Connection settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/configuration');
Navigator.of(context).pushNamed('/connection-settings', arguments: {"homeAssistant", widget.homeAssistant});
},
),
)
]);
menuItems.addAll([
Divider(),
new ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text("Log"),
@ -358,7 +422,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
},
),
new ListTile(
leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")),
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
title: Text("Report an issue"),
onTap: () {
Navigator.of(context).pop();
@ -366,6 +430,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
},
),
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(
aboutBoxChildren: <Widget>[
GestureDetector(
@ -383,11 +455,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
)
],
applicationName: appName,
applicationVersion: appVersion,
applicationLegalese: "build $appBuild",
applicationVersion: appVersion
)
]);
}
return new Drawer(
child: ListView(
children: menuItems,
@ -440,7 +510,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
child: Text("Retry", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
_reLoad();
},
);
break;
@ -458,12 +528,32 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
break;
}
case 6: {
case 60: {
_bottomBarAction = FlatButton(
child: Text("Settings", style: textStyle),
child: Text("Login", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
Navigator.pushNamed(context, '/connection-settings');
_reLoad();
},
);
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;
@ -474,52 +564,51 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
child: Text("Refresh", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
_reLoad();
},
);
break;
}
case 82:
case 81:
case 8: {
_bottomBarAction = FlatButton(
child: Text("Reconnect", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
_reLoad();
},
);
break;
}
default: {
_bottomBarAction = FlatButton(
child: Text("Reload", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
},
);
_bottomBarAction = Container(width: 0.0, height: 0.0,);
break;
}
}
setState(() {
_bottomBarProgress = false;
_bottomBarText = "$message (code: $errorCode)";
_bottomBarText = "$message";
_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>();
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(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
@ -527,22 +616,23 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
floating: true,
pinned: true,
primary: true,
title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""),
title: Text(widget.homeAssistant.locationName ?? ""),
actions: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Colors.white,),
onPressed: () {
showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
context: context,
items: [PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
)]
items: popupMenuItems
).then((String val) {
if (val == "reload") {
_refreshData();
_reLoad();
} else if (val == "logout") {
widget.homeAssistant.logout().then((_) {
_reLoad();
});
}
});
}
@ -552,12 +642,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
setState(() {
_accountMenuExpanded = false;
});
},
),
bottom: empty ? null : TabBar(
controller: _viewsTabController,
tabs: buildUIViewTabs(),
isScrollable: true,
),
@ -571,18 +659,20 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
MaterialDesignIcons.getIconDataFromIconName("mdi:border-none-variant"),
size: 100.0,
color: Colors.blue,
color: Colors.black26,
),
]
),
)
:
_homeAssistant.buildViews(context, _useLovelaceUI),
widget.homeAssistant.buildViews(context, _viewsTabController),
);
}
TabController _viewsTabController;
@override
Widget build(BuildContext context) {
Widget bottomBar;
@ -634,7 +724,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}
}
// This method is rerun every time setState is called.
if (_homeAssistant.ui == null || _homeAssistant.ui.views == null) {
if (widget.homeAssistant.isNoViews) {
return Scaffold(
key: _scaffoldKey,
primary: false,
@ -648,9 +738,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
drawer: _buildAppDrawer(),
primary: false,
bottomNavigationBar: bottomBar,
body: DefaultTabController(
length: _homeAssistant.ui?.views?.length ?? 0,
body: HomeAssistantModel(
child: _buildScaffoldBody(false),
homeAssistant: widget.homeAssistant
),
);
}
@ -658,13 +748,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
@override
void dispose() {
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.dispose();
WidgetsBinding.instance.removeObserver(this);
if (_stateSubscription != null) _stateSubscription.cancel();
if (_settingsSubscription != null) _settingsSubscription.cancel();
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
if (_showErrorSubscription != null) _showErrorSubscription.cancel();
_homeAssistant.disconnect();
_viewsTabController?.dispose();
_stateSubscription?.cancel();
_settingsSubscription?.cancel();
_serviceCallSubscription?.cancel();
_showEntityPageSubscription?.cancel();
_showErrorSubscription?.cancel();
_startAuthSubscription?.cancel();
_reloadUISubscription?.cancel();
//TODO disconnect
//widget.homeAssistant?.disconnect();
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 _hassioPort = "";
String _newHassioPort = "";
String _hassioPassword = "";
String _newHassioPassword = "";
String _socketProtocol = "wss";
String _newSocketProtocol = "wss";
bool _useLovelace = true;
bool _newUseLovelace = true;
String oauthUrl;
@override
void initState() {
super.initState();
_loadSettings();
}
_loadSettings() async {
@ -33,7 +34,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
setState(() {
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
try {
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
@ -44,7 +44,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
}
bool _checkConfigChanged() {
return ((_newHassioPassword != _hassioPassword) ||
return (
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
@ -59,7 +59,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("hassio-domain", _newHassioDomain);
prefs.setString("hassio-port", _newHassioPort);
prefs.setString("hassio-password", _newHassioPassword);
prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
prefs.setBool("use-lovelace", _newUseLovelace);
@ -149,21 +148,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
"Try ports 80 and 443 if default is not working and you don't know why.",
style: TextStyle(color: Colors.grey),
),
new TextField(
decoration: InputDecoration(
labelText: "Access token"
),
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _newHassioPassword,
selection:
new TextSelection.collapsed(offset: _newHassioPassword.length)
)
),
onChanged: (value) {
_newHassioPassword = value;
}
),
Padding(
padding: EdgeInsets.only(top: 20.0),
child: Text(

View File

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

View File

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

View File

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

View File

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

View File

@ -47,12 +47,44 @@ class Logger {
class HAUtils {
static void launchURL(String url) async {
if (await canLaunch(url)) {
await launch(url);
if (await urlLauncher.canLaunch(url)) {
await urlLauncher.launch(url);
} else {
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 {
@ -77,6 +109,16 @@ class RefreshDataFinishedEvent {
RefreshDataFinishedEvent();
}
class ReloadUIEvent {
ReloadUIEvent();
}
class StartAuthEvent {
String oauthUrl;
StartAuthEvent(this.oauthUrl);
}
class ServiceCallEvent {
String domain;
String service;

View File

@ -35,7 +35,7 @@ packages:
name: cached_network_image
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.0-alpha.2"
version: "0.7.0"
charcode:
dependency: transitive
description:
@ -49,14 +49,14 @@ packages:
name: charts_common
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
version: "0.6.0"
charts_flutter:
dependency: "direct main"
description:
name: charts_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
version: "0.6.0"
collection:
dependency: transitive
description:
@ -100,7 +100,14 @@ packages:
name: event_bus
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
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:
dependency: "direct main"
description: flutter
@ -112,7 +119,14 @@ packages:
name: flutter_cache_manager
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0-beta.2+1"
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:
dependency: "direct dev"
description:
@ -127,11 +141,32 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: "direct main"
description:
name: flutter_svg
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.4"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_webview_plugin:
dependency: "direct main"
description:
name: flutter_webview_plugin
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.1"
http:
dependency: transitive
description:
@ -195,13 +230,34 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.2"
path_drawing:
dependency: transitive
description:
name: path_drawing
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
path_parsing:
dependency: transitive
description:
name: path_parsing
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
path_provider:
dependency: transitive
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.1"
version: "0.5.0+1"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
petitparser:
dependency: transitive
description:
@ -209,6 +265,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
progress_indicators:
dependency: "direct main"
description:
@ -229,7 +292,7 @@ packages:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.1+1"
version: "0.5.1+2"
sky_engine:
dependency: transitive
description: flutter
@ -241,14 +304,14 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.1"
version: "1.5.4"
sqflite:
dependency: transitive
description:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.2+1"
version: "1.1.3"
stack_trace:
dependency: transitive
description:
@ -276,21 +339,21 @@ packages:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.3+2"
version: "2.1.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
version: "1.1.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1"
version: "0.2.2"
typed_data:
dependency: transitive
description:
@ -304,14 +367,14 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.1"
version: "5.0.2"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
version: "2.0.0"
vector_math:
dependency: transitive
description:
@ -332,7 +395,7 @@ packages:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.1"
version: "3.2.5"
yaml:
dependency: transitive
description:
@ -341,5 +404,5 @@ packages:
source: hosted
version: "2.1.15"
sdks:
dart: ">=2.0.0 <3.0.0"
flutter: ">=0.5.6 <2.0.0"
dart: ">=2.1.0 <3.0.0"
flutter: ">=1.2.1 <2.0.0"

View File

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

File diff suppressed because one or more lines are too long