Compare commits

...

16 Commits

15 changed files with 143 additions and 150 deletions

View File

@ -7,35 +7,16 @@ assignees: ''
---
<!--
Please provide as much information as possible.
-->
**HA Client version:** <!-- Main app menu => About HA Client -->
**HA Client version:** [Main menu -> About HA Client]
**Home Assistant version:** <!-- 0.94.1 for example -->
**Home Assistant version:**
**Device name:** <!-- Pixel 2 for example -->
**Device name:**
**Android version:** <!-- 8.1 for example -->
**Connection type:** <!-- For example "Local IP" or "Remote UI" or "Own domain"-->
**Login type:** <!-- For example "HA Login" or "Manual token"-->
**Android version:**
**Description**
<!--
Describe your issue here
-->
[Replace with description]
**Screenshots**
<!--
Please provide screenshots if it is a UI issue. Also you can attach screenshot from Home Assistant web UI as an expected result
-->
**Logs**
<!--
Right after issue reproduced go to app menu and tap "Log". Copy log with a "Copy" button in the upper-right corner and post it below
-->
```
[Replace this text with your logs]
```
[Replace with screenshots]

View File

@ -285,7 +285,9 @@ class CardWidget extends StatelessWidget {
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: EntityButtonCardBody(),
child: EntityButtonCardBody(
showName: card.showName,
),
handleTap: true
)
);

View File

@ -2,8 +2,10 @@ part of '../../main.dart';
class EntityButtonCardBody extends StatelessWidget {
final bool showName;
EntityButtonCardBody({
Key key,
Key key, this.showName: true,
}) : super(key: key);
@override
@ -39,13 +41,16 @@ class EntityButtonCardBody extends StatelessWidget {
}
Widget _buildName() {
return EntityName(
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
maxLines: 3,
wordsWrap: true,
textAlign: TextAlign.center,
fontSize: Sizes.nameFontSize,
);
if (showName) {
return EntityName(
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
maxLines: 3,
wordsWrap: true,
textAlign: TextAlign.center,
fontSize: Sizes.nameFontSize,
);
}
return Container(width: 0, height: 0);
}
}

View File

@ -58,7 +58,7 @@ class EntityWrapper {
}
case EntityUIAction.navigate: {
if (uiAction.tapService.startsWith("/")) {
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
@ -98,7 +98,7 @@ class EntityWrapper {
}
case EntityUIAction.navigate: {
if (uiAction.holdService.startsWith("/")) {
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {

View File

@ -84,25 +84,22 @@ class MediaPlayerEntity extends Entity {
}
bool canCalculateActualPosition() {
return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds >= 0;
return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds > 0;
}
double getActualPosition() {
double result = 0;
if (canCalculateActualPosition()) {
Duration durationD;
Duration positionD;
durationD = Duration(seconds: durationSeconds);
positionD = Duration(
Duration durationD;
Duration positionD;
durationD = Duration(seconds: durationSeconds);
positionD = Duration(
seconds: positionSeconds);
result = positionD.inSeconds.toDouble();
int differenceInSeconds = DateTime
result = positionD.inSeconds.toDouble();
int differenceInSeconds = DateTime
.now()
.difference(positionLastUpdated)
.inSeconds;
result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble();
}
result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble();
return result;
}

View File

@ -22,13 +22,13 @@ class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> {
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
double progress;
double progress = 0;
int currentPosition;
if (entity.canCalculateActualPosition()) {
currentPosition = entity.getActualPosition().toInt();
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
} else {
progress = 0;
if (currentPosition > 0) {
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
}
}
return LinearProgressIndicator(
value: progress,

View File

@ -460,7 +460,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
}
void _duplicateTo(entity) {
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
if (entity.canCalculateActualPosition()) {
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
} else {
HomeAssistant().savedPlayerPosition = 0;
}
Navigator.of(context).pushNamed("/play-media", arguments: {
"url": entity.attributes["media_content_id"],
"type": entity.attributes["media_content_type"]

View File

@ -63,10 +63,6 @@ class HomeAssistant {
futures.add(_getServices());
futures.add(_getUserInfo());
futures.add(_getPanels());
futures.add(ConnectionManager().sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "state_changed"},
));
Future.wait(futures).then((_) {
if (isMobileAppEnabled) {
if (!childMode) _createUI();
@ -223,9 +219,9 @@ class HomeAssistant {
name: rawCardInfo["title"] ?? rawCardInfo["name"],
type: rawCardInfo['type'] ?? CardType.ENTITIES,
columnsCount: rawCardInfo['columns'] ?? 4,
showName: rawCardInfo['show_name'] ?? true,
showState: rawCardInfo['show_state'] ?? true,
showEmpty: rawCardInfo['show_empty'] ?? true,
showName: (rawCardInfo['show_name'] ?? rawCard['show_name']) ?? true,
showState: (rawCardInfo['show_state'] ?? rawCard['show_state']) ?? true,
showEmpty: (rawCardInfo['show_empty'] ?? rawCard['show_empty']) ?? true,
stateFilter: (rawCard['state_filter'] ?? rawCardInfo['state_filter']) ?? [],
states: rawCardInfo['states'],
conditions: rawCard['conditions'] ?? [],

View File

@ -143,7 +143,7 @@ final SentryClient _sentry = SentryClient(dsn: "https://03ef364745cc4c23a60ddbc8
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = "HA Client";
const appVersionNumber = "0.7.5";
const appVersionNumber = "0.7.6";
const appVersionAdd = "";
const appVersion = "$appVersionNumber$appVersionAdd";
@ -176,12 +176,7 @@ void main() async {
};
runZoned(() {
workManager.Workmanager.initialize(
updateDeviceLocationIsolate,
isInDebugMode: false
);
runApp(new HAClientApp());
}, onError: (error, stack) {
_reportError(error, stack);
});

View File

@ -98,16 +98,23 @@ class ConnectionManager {
void _doConnect({Completer completer, bool forceReconnect}) {
if (forceReconnect || !isConnected) {
_connect().timeout(connectTimeout, onTimeout: () {
_disconnect().then((_) {
if (completer != null && !completer.isCompleted) {
completer.completeError(HAError("Connection timeout"));
}
_disconnect().then((_){
_connect().timeout(connectTimeout).then((_) {
completer?.complete();
}).catchError((e) {
_disconnect().then((_) {
if (e is TimeoutException) {
if (connecting != null && !connecting.isCompleted) {
connecting.completeError(HAError("Connection timeout"));
}
completer?.completeError(HAError("Connection timeout"));
} else if (e is HAError) {
completer?.completeError(e);
} else {
completer?.completeError(HAError("${e.toString()}"));
}
});
});
}).then((_) {
completer?.complete();
}).catchError((e) {
completer?.completeError(e);
});
} else {
completer?.complete();
@ -124,40 +131,50 @@ class ConnectionManager {
connecting = Completer();
_disconnect().then((_) {
Logger.d("Socket connecting...");
_socket = IOWebSocketChannel.connect(
try {
_socket = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
_socketSubscription = _socket.stream.listen(
(message) {
isConnected = true;
var data = json.decode(message);
if (data["type"] == "auth_required") {
Logger.d("[Received] <== ${data.toString()}");
_authenticate().then((_) {
Logger.d('Authentication complete');
connecting.complete();
}).catchError((e) {
if (!connecting.isCompleted) connecting.completeError(e);
});
} else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.complete();
_messageResolver.remove("auth");
if (_token != null) {
if (!connecting.isCompleted) connecting.complete();
_socketSubscription = _socket.stream.listen(
(message) {
isConnected = true;
var data = json.decode(message);
if (data["type"] == "auth_required") {
Logger.d("[Received] <== ${data.toString()}");
_authenticate().then((_) {
Logger.d('Authentication complete');
connecting.complete();
}).catchError((e) {
if (!connecting.isCompleted) connecting.completeError(e);
});
} else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}");
Logger.d("[Connection] Subscribing to events");
sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "state_changed"},
).whenComplete((){
_messageResolver["auth"]?.complete();
_messageResolver.remove("auth");
if (_token != null) {
if (!connecting.isCompleted) connecting.complete();
}
});
} else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
_messageResolver.remove("auth");
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
} else {
_handleMessage(data);
}
} else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
_messageResolver.remove("auth");
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
} else {
_handleMessage(data);
}
},
cancelOnError: true,
onDone: () => _handleSocketClose(connecting),
onError: (e) => _handleSocketError(e, connecting)
);
},
cancelOnError: true,
onDone: () => _handleSocketClose(connecting),
onError: (e) => _handleSocketError(e, connecting)
);
} catch(exeption) {
connecting.completeError(HAError("${exeption.toString()}"));
}
});
return connecting.future;
}
@ -209,38 +226,24 @@ class ConnectionManager {
void _handleSocketClose(Completer connectionCompleter) {
Logger.d("Socket disconnected.");
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect().catchError((e) {
isConnected = false;
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
});
});
});
}
_disconnect().then((_) {
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
}
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
});
}
void _handleSocketError(e, Completer connectionCompleter) {
Logger.e("Socket stream Error: $e");
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect().catchError((e) {
isConnected = false;
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
});
});
});
}
_disconnect().then((_) {
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
}
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
});
}
Future _authenticate() {
@ -329,13 +332,13 @@ class ConnectionManager {
_messageResolver[callbackName] = _completer;
String rawMessage = json.encode(dataObject);
if (!isConnected) {
_connect().timeout(connectTimeout, onTimeout: (){
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
}).then((_) {
_connect().timeout(connectTimeout).then((_) {
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
_socket.sink.add(rawMessage);
}).catchError((e) {
_completer.completeError(e);
if (!_completer.isCompleted) {
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
}
});
} else {
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
@ -363,12 +366,12 @@ class ConnectionManager {
sendHTTPPost(
endPoint: "/api/services/$domain/$service",
data: json.encode(serviceData)
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}")));
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString())));
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
else
sendHTTPPost(
endPoint: "/api/services/$domain/$service"
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}")));
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString())));
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
return completer.future;
}
@ -414,7 +417,7 @@ class ConnectionManager {
completer.complete(response.body);
} else {
Logger.d("[Received] <== HTTP ${response.statusCode}: ${response.body}");
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
completer.completeError(response);
}
}).catchError((e) {
completer.completeError(e);

View File

@ -82,8 +82,8 @@ class LocationManager {
int delay = i*delayFactor;
Logger.d("Scheduling location update task #$i for every ${interval.inMinutes} minutes in $delay minutes...");
await workManager.Workmanager.registerPeriodicTask(
"$backgroundTaskId$n",
"haClientLocationTracking-0$n",
"$backgroundTaskId$i",
"haClientLocationTracking-0$i",
tag: backgroundTaskTag,
inputData: {
"webhookId": webhookId,

View File

@ -81,7 +81,7 @@ class MobileAppIntegrationManager {
}
completer.complete();
}).catchError((e) {
if (e['code'] != null && e['code'] == 410) {
if (e is http.Response && e.statusCode == 410) {
Logger.e("MobileApp integration was removed");
_askToRegisterApp();
} else {

View File

@ -30,12 +30,16 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
@override
void initState() {
super.initState();
final Stream purchaseUpdates =
InAppPurchaseConnection.instance.purchaseUpdatedStream;
_subscription = purchaseUpdates.listen((purchases) {
_handlePurchaseUpdates(purchases);
});
super.initState();
workManager.Workmanager.initialize(
updateDeviceLocationIsolate,
isInDebugMode: false
);
enableShareReceiving();
WidgetsBinding.instance.addObserver(this);
@ -328,7 +332,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
void _showEntityPage(String entityId) {
setState(() {
_entityToShow = HomeAssistant().entities.get(entityId);
_entityToShow = HomeAssistant().entities?.get(entityId);
if (_entityToShow != null) {
_mainScrollController?.jumpTo(0);
}

View File

@ -75,10 +75,16 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
_saveSettings() async {
_newHassioDomain = _newHassioDomain.trim();
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
if (_newHassioDomain.startsWith("http") && _newHassioDomain.indexOf("//") > 0) {
_newHassioDomain.startsWith("https") ? _newSocketProtocol = "wss" : _newSocketProtocol = "ws";
_newHassioDomain = _newHassioDomain.split("//")[1];
}
_newHassioDomain = _newHassioDomain.split("/")[0];
if (_newHassioDomain.contains(":")) {
List<String> domainAndPort = _newHassioDomain.split(":");
_newHassioDomain = domainAndPort[0];
_newHassioPort = domainAndPort[1];
}
SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage();
if (_newLongLivedToken.isNotEmpty) {

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 0.7.5+750
version: 0.7.6+760
environment:
@ -27,7 +27,7 @@ dependencies:
device_info: ^0.4.0+3
flutter_local_notifications: ^0.8.4
geolocator: ^5.1.5
workmanager: ^0.1.3
workmanager: ^0.1.5
battery: ^0.3.1+1
sentry: ^2.3.1
share: