Compare commits

..

3 Commits

Author SHA1 Message Date
5211d1ff46 v. 0.5.4 2019-05-09 14:15:21 +03:00
d1c9fddba6 v. 0.5.4 2019-05-09 13:17:30 +03:00
14cc55a2c7 Resolves #373 2019-05-09 13:16:35 +03:00
41 changed files with 548 additions and 865 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -73,7 +73,6 @@ class Entity {
Map attributes; Map attributes;
String domain; String domain;
String entityId; String entityId;
String entityPicture;
String state; String state;
String displayState; String displayState;
DateTime _lastUpdated; DateTime _lastUpdated;
@ -95,6 +94,7 @@ class Entity {
bool get isBadge => Entity.badgeDomains.contains(domain); bool get isBadge => Entity.badgeDomains.contains(domain);
String get icon => attributes["icon"] ?? ""; String get icon => attributes["icon"] ?? "";
bool get isOn => state == EntityState.on; bool get isOn => state == EntityState.on;
String get entityPicture => _getEntityPictureUrl();
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? ""; String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
List get childEntityIds => attributes["entity_id"] ?? []; List get childEntityIds => attributes["entity_id"] ?? [];
String get lastUpdated => _getLastUpdatedFormatted(); String get lastUpdated => _getLastUpdatedFormatted();
@ -102,21 +102,21 @@ class Entity {
double get doubleState => double.tryParse(state) ?? 0.0; double get doubleState => double.tryParse(state) ?? 0.0;
int get supportedFeatures => attributes["supported_features"] ?? 0; int get supportedFeatures => attributes["supported_features"] ?? 0;
String _getEntityPictureUrl(String webHost) { String _getEntityPictureUrl() {
String result = attributes["entity_picture"]; String result = attributes["entity_picture"];
if (result == null) return result; if (result == null) return result;
if (!result.startsWith("http")) { if (!result.startsWith("http")) {
if (result.startsWith("/")) { if (result.startsWith("/")) {
result = "$webHost$result"; result = "$homeAssistantWebHost$result";
} else { } else {
result = "$webHost/$result"; result = "$homeAssistantWebHost/$result";
} }
} }
return result; return result;
} }
Entity(Map rawData, String webHost) { Entity(Map rawData) {
update(rawData, webHost); update(rawData);
} }
Entity.missed(String entityId) { Entity.missed(String entityId) {
@ -148,7 +148,7 @@ class Entity {
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"}; attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
} }
void update(Map rawData, String webHost) { void update(Map rawData) {
attributes = rawData["attributes"] ?? {}; attributes = rawData["attributes"] ?? {};
domain = rawData["entity_id"].split(".")[0]; domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"]; entityId = rawData["entity_id"];
@ -156,7 +156,6 @@ class Entity {
state = rawData["state"]; state = rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state; displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
_lastUpdated = DateTime.tryParse(rawData["last_updated"]); _lastUpdated = DateTime.tryParse(rawData["last_updated"]);
entityPicture = _getEntityPictureUrl(webHost);
} }
double _getDoubleAttributeValue(String attributeName) { double _getDoubleAttributeValue(String attributeName) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
part of '../main.dart'; part of '../main.dart';
class TimerEntity extends Entity { class TimerEntity extends Entity {
TimerEntity(Map rawData, String webHost) : super(rawData, webHost); TimerEntity(Map rawData) : super(rawData);
Duration duration; Duration duration;
@override @override
void update(Map rawData, String webHost) { void update(Map rawData) {
super.update(rawData, webHost); super.update(rawData);
String durationSource = "${attributes["duration"]}"; String durationSource = "${attributes["duration"]}";
if (durationSource != null && durationSource.isNotEmpty) { if (durationSource != null && durationSource.isNotEmpty) {
try { try {

View File

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

View File

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

View File

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

View File

@ -47,7 +47,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
} }
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) { if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
_historyLastUpdated = now; _historyLastUpdated = now;
ha.connection.getHistory(entityId).then((history){ ha.getHistory(entityId).then((history){
if (!_disposed) { if (!_disposed) {
setState(() { setState(() {
_history = history.isNotEmpty ? history[0] : []; _history = history.isNotEmpty ? history[0] : [];

View File

@ -1,27 +1,37 @@
part of 'main.dart'; part of 'main.dart';
class HomeAssistant { class HomeAssistant {
String _webSocketAPIEndpoint;
final Connection connection = Connection(); String _password;
bool _useLovelace = false; bool _useLovelace = false;
//bool isSettingsLoaded = false;
IOWebSocketChannel _hassioChannel;
SendMessageQueue _messageQueue;
int _currentMessageId = 0;
int _subscriptionMessageId = 0;
Map<int, Completer> _messageResolver = {};
EntityCollection entities; EntityCollection entities;
HomeAssistantUI ui; HomeAssistantUI ui;
Map _instanceConfig = {}; Map _instanceConfig = {};
String _userName; String _userName;
String hostname;
HSVColor savedColor; HSVColor savedColor;
Map _rawLovelaceData; Map _rawLovelaceData;
List<Panel> panels = []; List<Panel> panels = [];
Completer _fetchCompleter;
Completer _connectionCompleter;
Timer _connectionTimer;
Timer _fetchTimer;
bool autoReconnect = false;
StreamSubscription _socketSubscription;
int messageExpirationTime = 30; //seconds
Duration fetchTimeout = Duration(seconds: 30); Duration fetchTimeout = Duration(seconds: 30);
Duration connectTimeout = Duration(seconds: 15);
String get locationName { String get locationName {
if (_useLovelace) { if (_useLovelace) {
@ -32,39 +42,117 @@ class HomeAssistant {
} }
String get userName => _userName ?? locationName; String get userName => _userName ?? locationName;
String get userAvatarText => userName.length > 0 ? userName[0] : ""; String get userAvatarText => userName.length > 0 ? userName[0] : "";
bool get isNoEntities => entities == null || entities.isEmpty;
bool get isNoViews => ui == null || ui.isEmpty;
//int get viewsCount => entities.views.length ?? 0; //int get viewsCount => entities.views.length ?? 0;
HomeAssistant(); HomeAssistant() {
entities = EntityCollection();
Completer _connectCompleter; _messageQueue = SendMessageQueue(messageExpirationTime);
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;
} }
Completer _fetchCompleter; void updateSettings(String url, String password, bool useLovelace) {
_webSocketAPIEndpoint = url;
_password = password;
_useLovelace = useLovelace;
Logger.d( "Use lovelace is $_useLovelace");
}
Future fetch() { Future fetch() {
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) { if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
Logger.w("Previous data fetch is not completed yet"); 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);
});
}
return _fetchCompleter.future; return _fetchCompleter.future;
} }
_fetchCompleter = Completer();
disconnect() async {
if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) {
await _hassioChannel.sink.close().timeout(Duration(seconds: 3),
onTimeout: () => Logger.d( "Socket sink closed")
);
await _socketSubscription.cancel();
_hassioChannel = null;
}
}
Future _connection() {
if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
Logger.d("Previous connection is not complited");
} else {
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
_connectionCompleter = new Completer();
autoReconnect = false;
disconnect().then((_){
Logger.d( "Socket connecting...");
_connectionTimer = Timer(connectTimeout, () {
Logger.e( "Socket connection timeout");
_handleSocketError(null);
});
if (_socketSubscription != null) {
_socketSubscription.cancel();
}
_hassioChannel = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 30));
_socketSubscription = _hassioChannel.stream.listen(
(message) => _handleMessage(message),
cancelOnError: true,
onDone: () => _handleSocketClose(),
onError: (e) => _handleSocketError(e)
);
});
} else {
_completeConnecting(null);
}
}
return _connectionCompleter.future;
}
void _handleSocketClose() {
Logger.d("Socket disconnected. Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
}
}
void _handleSocketError(e) {
Logger.e("Socket stream Error: $e");
Logger.d("Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
} else {
disconnect().then((_) {
_completeConnecting({
"errorCode": 1,
"errorMessage": "Couldn't connect to Home Assistant. Check network connection or connection settings."
});
});
}
}
void _reconnect() {
disconnect().then((_) {
_connection().catchError((e){
_completeConnecting(e);
});
});
}
_getData() async {
List<Future> futures = []; List<Future> futures = [];
futures.add(_getStates()); futures.add(_getStates());
if (_useLovelace) { if (_useLovelace) {
@ -74,63 +162,107 @@ class HomeAssistant {
futures.add(_getServices()); futures.add(_getServices());
futures.add(_getUserInfo()); futures.add(_getUserInfo());
futures.add(_getPanels()); futures.add(_getPanels());
Future.wait(futures).then((_) { try {
await Future.wait(futures);
_createUI(); _createUI();
_fetchCompleter.complete(); _completeFetching(null);
}).catchError((e) { } catch (error) {
_fetchCompleter.completeError(e); _completeFetching(error);
}); }
return _fetchCompleter.future;
} }
Future logout() async { void _completeFetching(error) {
Logger.d("Logging out..."); _fetchTimer.cancel();
await connection.logout().then((_) { _completeConnecting(error);
ui?.clear(); if (!_fetchCompleter.isCompleted) {
entities?.clear(); 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") {
_sendAuthMessage('{"type": "auth","access_token": "$_password"}');
} else if (data["type"] == "auth_ok") {
_completeConnecting(null);
_sendSubscribe();
} else if (data["type"] == "auth_invalid") {
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") {
Logger.d("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}");
_messageResolver[data["id"]]?.complete(data);
_messageResolver.remove(data["id"]);
} else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
_handleEntityStateChange(data["event"]["data"]);
} else if (data["event"] != null) {
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
} else {
Logger.e("Event is null: $message");
}
} else {
Logger.w("Unknown message type: $message");
}
}
void _sendSubscribe() {
_incrementMessageId();
_subscriptionMessageId = _currentMessageId;
_send('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
} }
Future _getConfig() async { Future _getConfig() async {
await connection.sendSocketMessage(type: "get_config").then((data) { await _sendInitialMessage("get_config").then((data) => _instanceConfig = Map.from(data["result"]));
_instanceConfig = Map.from(data);
}).catchError((e) {
throw {"errorCode": 1, "errorMessage": "Error getting config: $e"};
});
} }
Future _getStates() async { Future _getStates() async {
await connection.sendSocketMessage(type: "get_states").then( await _sendInitialMessage("get_states").then((data) => entities.parse(data["result"]));
(data) => entities.parse(data)
).catchError((e) {
throw {"errorCode": 1, "errorMessage": "Error getting states: $e"};
});
} }
Future _getLovelace() async { Future _getLovelace() async {
await connection.sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) { await _sendInitialMessage("lovelace/config").then((data) => _rawLovelaceData = data["result"]);
throw {"errorCode": 1, "errorMessage": "Error getting lovelace config: $e"};
});
} }
Future _getUserInfo() async { Future _getUserInfo() async {
_userName = null; _userName = null;
await connection.sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) { await _sendInitialMessage("auth/current_user").then((data) => _userName = data["result"]["name"]);
Logger.w("Can't get user info: ${e}");
});
} }
Future _getServices() async { Future _getServices() async {
await connection.sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) { await _sendInitialMessage("get_services").then((data) => Logger.d("We actually don`t need the list of servcies for now"));
Logger.w("Can't get services: ${e}");
});
} }
Future _getPanels() async { Future _getPanels() async {
panels.clear(); panels.clear();
await connection.sendSocketMessage(type: "get_panels").then((data) { await _sendInitialMessage("get_panels").then((data) {
data.forEach((k,v) { if (data["success"]) {
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}"; data["result"].forEach((k,v) {
String title = v['title'] == null ? "${k[0]?.toUpperCase()}${k?.substring(1)}" : "${v['title'][0]?.toUpperCase()}${v['title']?.substring(1)}";
panels.add(Panel( panels.add(Panel(
id: k, id: k,
type: v["component_name"], type: v["component_name"],
@ -141,11 +273,85 @@ class HomeAssistant {
) )
); );
}); });
}).catchError((e) { }
throw {"errorCode": 1, "errorMessage": "Error getting panels list: $e"};
}); });
} }
_incrementMessageId() {
_currentMessageId += 1;
}
void _sendAuthMessage(String message) {
Logger.d( "[Sending] ==> auth request");
_hassioChannel.sink.add(message);
}
Future _sendInitialMessage(String type) {
Completer _completer = Completer();
_incrementMessageId();
_messageResolver[_currentMessageId] = _completer;
_send('{"id": $_currentMessageId, "type": "$type"}', false);
return _completer.future;
}
_send(String message, bool queued) {
var sendCompleter = Completer();
if (queued) _messageQueue.add(message);
_connection().then((r) {
_messageQueue.getActualMessages().forEach((message){
Logger.d( "[Sending queued] ==> $message");
_hassioChannel.sink.add(message);
});
if (!queued) {
Logger.d( "[Sending] ==> $message");
_hassioChannel.sink.add(message);
}
sendCompleter.complete();
}).catchError((e){
sendCompleter.completeError(e);
});
return sendCompleter.future;
}
Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
_incrementMessageId();
String message = "";
if (entityId != null) {
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
if (additionalParams != null) {
additionalParams.forEach((name, value) {
if ((value is double) || (value is int) || (value is List)) {
message += ', "$name" : $value';
} else {
message += ', "$name" : "$value"';
}
});
}
message += '}}';
} else {
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service"';
if (additionalParams != null && additionalParams.isNotEmpty) {
message += ', "service_data": {';
bool first = true;
additionalParams.forEach((name, value) {
if (!first) {
message += ', ';
}
if ((value is double) || (value is int) || (value is List)) {
message += '"$name" : $value';
} else {
message += '"$name" : "$value"';
}
first = false;
});
message += '}';
}
message += '}';
}
return _send(message, true);
}
void _handleEntityStateChange(Map eventData) { void _handleEntityStateChange(Map eventData) {
//TheLogger.debug( "New state for ${eventData['entity_id']}"); //TheLogger.debug( "New state for ${eventData['entity_id']}");
Map data = Map.from(eventData); Map data = Map.from(eventData);
@ -349,12 +555,31 @@ class HomeAssistant {
} }
} }
Widget buildViews(BuildContext context, TabController tabController) { Widget buildViews(BuildContext context, bool lovelace, TabController tabController) {
return ui.build(context, tabController); return ui.build(context, tabController);
} }
Future<List> getHistory(String entityId) async {
DateTime now = DateTime.now();
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
Logger.d("[Sending] ==> $url");
http.Response historyResponse;
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_password",
"Content-Type": "application/json"
});
var history = json.decode(historyResponse.body);
if (history is List) {
Logger.d( "[Received] <== ${history.first.length} history recors");
return history;
} else {
return [];
}
}
} }
/*
class SendMessageQueue { class SendMessageQueue {
int _messageTimeout; int _messageTimeout;
List<HAMessage> _queue = []; List<HAMessage> _queue = [];
@ -393,4 +618,4 @@ class HAMessage {
bool isExpired() { bool isExpired() {
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout; return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
} }
}*/ }

View File

@ -17,9 +17,6 @@ import 'package:progress_indicators/progress_indicators.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart'; import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
part 'entity_class/const.dart'; part 'entity_class/const.dart';
part 'entity_class/entity.class.dart'; part 'entity_class/entity.class.dart';
@ -90,8 +87,6 @@ part 'entity.page.dart';
part 'utils.class.dart'; part 'utils.class.dart';
part 'mdi.class.dart'; part 'mdi.class.dart';
part 'entity_collection.class.dart'; part 'entity_collection.class.dart';
part 'auth_manager.class.dart';
part 'connection.class.dart';
part 'ui_class/ui.dart'; part 'ui_class/ui.dart';
part 'ui_class/view.class.dart'; part 'ui_class/view.class.dart';
part 'ui_class/card.class.dart'; part 'ui_class/card.class.dart';
@ -105,7 +100,9 @@ part 'ui_widgets/config_panel_widget.dart';
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
const String appName = "HA Client"; const String appName = "HA Client";
const appVersion = "0.6.0-alpha2"; const appVersion = "0.5.4";
String homeAssistantWebHost;
void main() { void main() {
FlutterError.onError = (errorDetails) { FlutterError.onError = (errorDetails) {
@ -127,8 +124,6 @@ void main() {
} }
class HAClientApp extends StatelessWidget { class HAClientApp extends StatelessWidget {
final HomeAssistant homeAssistant = HomeAssistant();
// This widget is the root of your application. // This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -139,7 +134,7 @@ class HAClientApp extends StatelessWidget {
), ),
initialRoute: "/", initialRoute: "/",
routes: { routes: {
"/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,), "/": (context) => MainPage(title: 'HA Client'),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"), "/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
"/configuration": (context) => PanelPage(title: "Configuration"), "/configuration": (context) => PanelPage(title: "Configuration"),
"/log-view": (context) => LogViewPage(title: "Log") "/log-view": (context) => LogViewPage(title: "Log")
@ -149,84 +144,82 @@ class HAClientApp extends StatelessWidget {
} }
class MainPage extends StatefulWidget { class MainPage extends StatefulWidget {
MainPage({Key key, this.title, this.homeAssistant}) : super(key: key); MainPage({Key key, this.title}) : super(key: key);
final String title; final String title;
final HomeAssistant homeAssistant;
@override @override
_MainPageState createState() => new _MainPageState(); _MainPageState createState() => new _MainPageState();
} }
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin { class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
HomeAssistant _homeAssistant;
//Map _instanceConfig;
String _webSocketApiEndpoint;
String _password;
//int _uiViewsCount = 0;
String _instanceHost;
StreamSubscription _stateSubscription; StreamSubscription _stateSubscription;
StreamSubscription _settingsSubscription; StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription; StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription; StreamSubscription _showEntityPageSubscription;
StreamSubscription _showErrorSubscription; StreamSubscription _showErrorSubscription;
StreamSubscription _startAuthSubscription; bool _settingsLoaded = false;
StreamSubscription _reloadUISubscription; bool _accountMenuExpanded = false;
bool _useLovelaceUI;
int _previousViewCount; int _previousViewCount;
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
//widget.homeAssistant = HomeAssistant(); _settingsLoaded = false;
//_settingsLoaded = false;
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
Logger.d("<!!!> Creating new HomeAssistant instance");
_homeAssistant = HomeAssistant();
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) { _settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
Logger.d("Settings change event: reconnect=${event.reconnect}"); Logger.d("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) { if (event.reconnect) {
_reLoad(); _homeAssistant.disconnect().then((_){
_initialLoad();
});
} }
}); });
_initialLoad(); _initialLoad();
} }
void _initialLoad() { void _initialLoad() {
_showInfoBottomBar(progress: true,); _loadConnectionSettings().then((_){
_subscribe(); _subscribe();
widget.homeAssistant.init().then((_){ _refreshData();
_fetchData(); }, onError: (_) {
}, onError: (e) { _showErrorBottomBar(message: _, errorCode: 5);
_setErrorState(e);
}); });
} }
void _reLoad() {
_hideBottomBar();
_showInfoBottomBar(progress: true,);
widget.homeAssistant.init().then((_){
_fetchData();
}, onError: (e) {
_setErrorState(e);
});
}
_fetchData() async {
await widget.homeAssistant.fetch().then((_) {
_hideBottomBar();
int currentViewCount = widget.homeAssistant.ui?.views?.length ?? 0;
if (_previousViewCount != currentViewCount) {
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
_viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount;
}
}).catchError((e) {
_setErrorState(e);
});
eventBus.fire(RefreshDataFinishedEvent());
}
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
Logger.d("$state"); Logger.d("$state");
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed && _settingsLoaded) {
_reLoad(); _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;
} }
} }
@ -235,17 +228,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) { if (event.needToRebuildUI) {
Logger.d("New entity. Need to rebuild UI"); Logger.d("New entity. Need to rebuild UI");
_reLoad(); _refreshData();
} else { } else {
setState(() {}); setState(() {});
} }
}); });
} }
if (_reloadUISubscription == null) {
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
_reLoad();
});
}
if (_serviceCallSubscription == null) { if (_serviceCallSubscription == null) {
_serviceCallSubscription = _serviceCallSubscription =
eventBus.on<ServiceCallEvent>().listen((event) { eventBus.on<ServiceCallEvent>().listen((event) {
@ -266,51 +254,24 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_showErrorBottomBar(message: event.text, errorCode: event.errorCode); _showErrorBottomBar(message: event.text, errorCode: event.errorCode);
}); });
} }
}
if (_startAuthSubscription == null) { _refreshData() async {
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){ _homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI);
_showOAuth(); _hideBottomBar();
_showInfoBottomBar(progress: true,);
await _homeAssistant.fetch().then((result) {
_hideBottomBar();
int currentViewCount = _homeAssistant.ui?.views?.length ?? 0;
if (_previousViewCount != currentViewCount) {
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
_viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount;
}
}).catchError((e) {
_setErrorState(e);
}); });
} eventBus.fire(RefreshDataFinishedEvent());
/*_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");
}
);*/
}
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) { _setErrorState(e) {
@ -318,7 +279,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
Logger.e(e.toString()); Logger.e(e.toString());
Logger.e("${e.stackTrace}"); Logger.e("${e.stackTrace}");
_showErrorBottomBar( _showErrorBottomBar(
message: "Unknown error", message: "There was some error",
errorCode: 13 errorCode: 13
); );
} else { } else {
@ -329,19 +290,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
} }
void _callService(String domain, String service, String entityId, Map additionalParams) { void _callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
_showInfoBottomBar( _showInfoBottomBar(
message: "Calling $domain.$service", message: "Calling $domain.$service",
duration: Duration(seconds: 3) duration: Duration(seconds: 3)
); );
widget.homeAssistant.connection.callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e)); _homeAssistant.callService(domain, service, entityId, additionalParams).catchError((e) => _setErrorState(e));
} }
void _showEntityPage(String entityId) { void _showEntityPage(String entityId) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: widget.homeAssistant), builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: _homeAssistant),
) )
); );
} }
@ -349,8 +310,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
List<Tab> buildUIViewTabs() { List<Tab> buildUIViewTabs() {
List<Tab> result = []; List<Tab> result = [];
if (widget.homeAssistant.ui.views.isNotEmpty) { if (_homeAssistant.ui.views.isNotEmpty) {
widget.homeAssistant.ui.views.forEach((HAView view) { _homeAssistant.ui.views.forEach((HAView view) {
result.add(view.buildTab()); result.add(view.buildTab());
}); });
} }
@ -362,16 +323,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
List<Widget> menuItems = []; List<Widget> menuItems = [];
menuItems.add( menuItems.add(
UserAccountsDrawerHeader( UserAccountsDrawerHeader(
accountName: Text(widget.homeAssistant.userName), accountName: Text(_homeAssistant.userName),
accountEmail: Text(widget.homeAssistant.hostname ?? "Not configured"), accountEmail: Text(_instanceHost ?? "Not configured"),
/*onDetailsPressed: () { onDetailsPressed: () {
setState(() { setState(() {
_accountMenuExpanded = !_accountMenuExpanded; _accountMenuExpanded = !_accountMenuExpanded;
}); });
},*/ },
currentAccountPicture: CircleAvatar( currentAccountPicture: CircleAvatar(
child: Text( child: Text(
widget.homeAssistant.userAvatarText, _homeAssistant.userAvatarText,
style: TextStyle( style: TextStyle(
fontSize: 32.0 fontSize: 32.0
), ),
@ -379,8 +340,21 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
), ),
) )
); );
if (widget.homeAssistant.panels.isNotEmpty) { if (_accountMenuExpanded) {
widget.homeAssistant.panels.forEach((Panel panel) { menuItems.addAll([
ListTile(
leading: Icon(Icons.settings),
title: Text("Settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/connection-settings');
},
),
Divider(),
]);
} else {
if (_homeAssistant != null && _homeAssistant.panels.isNotEmpty) {
_homeAssistant.panels.forEach((Panel panel) {
if (!panel.isHidden) { if (!panel.isHidden) {
menuItems.add( menuItems.add(
new ListTile( new ListTile(
@ -391,28 +365,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
); );
} }
}); });
} menuItems.addAll([
//TODO check for loaded
menuItems.add(
new ListTile( new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")), leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
title: Text("Open Web UI"), title: Text("Open Web UI"),
onTap: () => HAUtils.launchURL(widget.homeAssistant.connection.httpWebHost), onTap: () => HAUtils.launchURL(homeAssistantWebHost),
) ),
); Divider()
menuItems.addAll([
Divider(),
ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
title: Text("Connection settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/connection-settings', arguments: {"homeAssistant", widget.homeAssistant});
},
)
]); ]);
}
menuItems.addAll([ menuItems.addAll([
Divider(),
new ListTile( new ListTile(
leading: Icon(Icons.insert_drive_file), leading: Icon(Icons.insert_drive_file),
title: Text("Log"), title: Text("Log"),
@ -490,6 +452,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
applicationVersion: appVersion applicationVersion: appVersion
) )
]); ]);
}
return new Drawer( return new Drawer(
child: ListView( child: ListView(
children: menuItems, children: menuItems,
@ -542,7 +505,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
child: Text("Retry", style: textStyle), child: Text("Retry", style: textStyle),
onPressed: () { onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar(); //_scaffoldKey?.currentState?.hideCurrentSnackBar();
_reLoad(); _refreshData();
}, },
); );
break; break;
@ -560,32 +523,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
break; break;
} }
case 60: { case 6: {
_bottomBarAction = FlatButton( _bottomBarAction = FlatButton(
child: Text("Login", style: textStyle), child: Text("Settings", style: textStyle),
onPressed: () { onPressed: () {
_reLoad(); //_scaffoldKey?.currentState?.hideCurrentSnackBar();
}, Navigator.pushNamed(context, '/connection-settings');
);
break;
}
case 63:
case 61: {
_bottomBarAction = FlatButton(
child: Text("Try again", style: textStyle),
onPressed: () {
_reLoad();
},
);
break;
}
case 62: {
_bottomBarAction = FlatButton(
child: Text("Login again", style: textStyle),
onPressed: () {
_reLoad();
}, },
); );
break; break;
@ -596,51 +539,52 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
child: Text("Refresh", style: textStyle), child: Text("Refresh", style: textStyle),
onPressed: () { onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar(); //_scaffoldKey?.currentState?.hideCurrentSnackBar();
_reLoad(); _refreshData();
}, },
); );
break; break;
} }
case 82:
case 81:
case 8: { case 8: {
_bottomBarAction = FlatButton( _bottomBarAction = FlatButton(
child: Text("Reconnect", style: textStyle), child: Text("Reconnect", style: textStyle),
onPressed: () { onPressed: () {
_reLoad(); //_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
}, },
); );
break; break;
} }
default: { default: {
_bottomBarAction = Container(width: 0.0, height: 0.0,); _bottomBarAction = FlatButton(
child: Text("Reload", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
},
);
break; break;
} }
} }
setState(() { setState(() {
_bottomBarProgress = false; _bottomBarProgress = false;
_bottomBarText = "$message"; _bottomBarText = "$message (code: $errorCode)";
_showBottomBar = true; _showBottomBar = true;
}); });
/*_scaffoldKey.currentState.hideCurrentSnackBar();
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text("$message (code: $errorCode)"),
action: action,
duration: Duration(hours: 1),
)
);*/
} }
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget _buildScaffoldBody(bool empty) { Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> popupMenuItems = [];
popupMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
));
if (widget.homeAssistant.connection.isAuthenticated) {
popupMenuItems.add(
PopupMenuItem<String>(
child: new Text("Logout"),
value: "logout",
));
}
return NestedScrollView( return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[ return <Widget>[
@ -648,7 +592,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
floating: true, floating: true,
pinned: true, pinned: true,
primary: true, primary: true,
title: Text(widget.homeAssistant.locationName ?? ""), title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""),
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
@ -657,14 +601,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
showMenu( showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0), position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
context: context, context: context,
items: popupMenuItems items: [PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
)]
).then((String val) { ).then((String val) {
if (val == "reload") { if (val == "reload") {
_reLoad(); _refreshData();
} else if (val == "logout") {
widget.homeAssistant.logout().then((_) {
_reLoad();
});
} }
}); });
} }
@ -674,6 +617,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
icon: Icon(Icons.menu), icon: Icon(Icons.menu),
onPressed: () { onPressed: () {
_scaffoldKey.currentState.openDrawer(); _scaffoldKey.currentState.openDrawer();
setState(() {
_accountMenuExpanded = false;
});
}, },
), ),
bottom: empty ? null : TabBar( bottom: empty ? null : TabBar(
@ -691,15 +637,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
MaterialDesignIcons.getIconDataFromIconName("mdi:border-none-variant"), MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant"),
size: 100.0, size: 100.0,
color: Colors.black26, color: Colors.blue,
), ),
] ]
), ),
) )
: :
widget.homeAssistant.buildViews(context, _viewsTabController), _homeAssistant.buildViews(context, _useLovelaceUI, _viewsTabController),
); );
} }
@ -756,7 +702,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
} }
// This method is rerun every time setState is called. // This method is rerun every time setState is called.
if (widget.homeAssistant.isNoViews) { if (_homeAssistant.ui == null || _homeAssistant.ui.views == null) {
return Scaffold( return Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
primary: false, primary: false,
@ -772,7 +718,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
bottomNavigationBar: bottomBar, bottomNavigationBar: bottomBar,
body: HomeAssistantModel( body: HomeAssistantModel(
child: _buildScaffoldBody(false), child: _buildScaffoldBody(false),
homeAssistant: widget.homeAssistant homeAssistant: _homeAssistant
), ),
); );
} }
@ -780,19 +726,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
@override @override
void dispose() { void dispose() {
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.dispose();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
_viewsTabController?.dispose(); _viewsTabController.dispose();
_stateSubscription?.cancel(); if (_stateSubscription != null) _stateSubscription.cancel();
_settingsSubscription?.cancel(); if (_settingsSubscription != null) _settingsSubscription.cancel();
_serviceCallSubscription?.cancel(); if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
_showEntityPageSubscription?.cancel(); if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
_showErrorSubscription?.cancel(); if (_showErrorSubscription != null) _showErrorSubscription.cancel();
_startAuthSubscription?.cancel(); _homeAssistant.disconnect();
_reloadUISubscription?.cancel();
//TODO disconnect
//widget.homeAssistant?.disconnect();
super.dispose(); super.dispose();
} }
} }

View File

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

View File

@ -36,8 +36,7 @@ class Panel {
) )
); );
} else { } else {
HomeAssistantModel haModel = HomeAssistantModel.of(context); String url = "$homeAssistantWebHost/$urlPath";
String url = "${haModel.homeAssistant.connection.httpWebHost}/$urlPath";
Logger.d("Launching custom tab with $url"); Logger.d("Launching custom tab with $url");
HAUtils.launchURLInCustomTab(context, url); HAUtils.launchURLInCustomTab(context, url);
} }

View File

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

View File

@ -85,7 +85,7 @@ class HAView {
} else { } else {
return return
Tab( Tab(
text: name.toUpperCase(), text: "${name?.toUpperCase()}",
); );
} }
} else { } else {
@ -99,7 +99,7 @@ class HAView {
); );
} else { } else {
return Tab( return Tab(
text: linkedEntity.displayName.toUpperCase(), text: "${linkedEntity?.displayName?.toUpperCase()}",
); );
} }

View File

@ -243,7 +243,7 @@ class CardWidget extends StatelessWidget {
Widget _buildEntityButtonCard(BuildContext context) { Widget _buildEntityButtonCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ?? card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
card.linkedEntityWrapper.displayName.toUpperCase(); card.linkedEntityWrapper.displayName?.toUpperCase();
return Card( return Card(
child: EntityModel( child: EntityModel(
entityWrapper: card.linkedEntityWrapper, entityWrapper: card.linkedEntityWrapper,

View File

@ -109,16 +109,6 @@ class RefreshDataFinishedEvent {
RefreshDataFinishedEvent(); RefreshDataFinishedEvent();
} }
class ReloadUIEvent {
ReloadUIEvent();
}
class StartAuthEvent {
String oauthUrl;
StartAuthEvent(this.oauthUrl);
}
class ServiceCallEvent { class ServiceCallEvent {
String domain; String domain;
String service; String service;

View File

@ -35,7 +35,7 @@ packages:
name: cached_network_image name: cached_network_image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.7.0" version: "0.8.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -100,14 +100,7 @@ packages:
name: event_bus name: event_bus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "1.1.0"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0+1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -141,13 +134,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.0" version: "0.2.0"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
flutter_svg: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
@ -160,20 +146,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_webview_plugin:
dependency: "direct main"
description:
name: flutter_webview_plugin
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.1"
http: http:
dependency: transitive dependency: transitive
description: description:
name: http name: http
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.0+1" version: "0.12.0+2"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -187,14 +166,14 @@ packages:
name: image name: image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.7" version: "2.0.8"
intl: intl:
dependency: transitive dependency: transitive
description: description:
name: intl name: intl
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.15.7" version: "0.15.8"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@ -208,7 +187,7 @@ packages:
name: markdown name: markdown
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.0.3"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -236,14 +215,14 @@ packages:
name: path_drawing name: path_drawing
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.0" version: "0.4.1"
path_parsing: path_parsing:
dependency: transitive dependency: transitive
description: description:
name: path_parsing name: path_parsing
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.3" version: "0.1.4"
path_provider: path_provider:
dependency: transitive dependency: transitive
description: description:
@ -265,13 +244,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
progress_indicators: progress_indicators:
dependency: "direct main" dependency: "direct main"
description: description:
@ -292,7 +264,7 @@ packages:
name: shared_preferences name: shared_preferences
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.1+2" version: "0.5.2"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -311,7 +283,7 @@ packages:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.3" version: "1.1.5"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -374,7 +346,7 @@ packages:
name: uuid name: uuid
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.0.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -388,7 +360,7 @@ packages:
name: web_socket_channel name: web_socket_channel
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.9" version: "1.0.12"
xml: xml:
dependency: transitive dependency: transitive
description: description:

View File

@ -1,7 +1,7 @@
name: hass_client name: hass_client
description: Home Assistant Android Client description: Home Assistant Android Client
version: 0.6.0+601 version: 0.5.4+541
environment: environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0" sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -20,9 +20,6 @@ dependencies:
flutter_markdown: any flutter_markdown: any
flutter_svg: ^0.10.3 flutter_svg: ^0.10.3
flutter_custom_tabs: ^0.6.0 flutter_custom_tabs: ^0.6.0
firebase_messaging: ^4.0.0+1
flutter_webview_plugin: ^0.3.1
flutter_secure_storage: ^3.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: