diff --git a/android/app/build.gradle b/android/app/build.gradle index 6eba0af..a3f1b09 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -39,8 +39,8 @@ android { applicationId "com.keyboardcrumbs.haclient" minSdkVersion 21 targetSdkVersion 27 - versionCode 18 - versionName "0.1.0-alpha" + versionCode 19 + versionName "0.1.1-alpha" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } diff --git a/lib/data_model.dart b/lib/data_model.dart index 51c9166..c46505f 100644 --- a/lib/data_model.dart +++ b/lib/data_model.dart @@ -46,10 +46,10 @@ class HassioDataModel { Future fetch() { if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) { - debugPrint("Previous fetch is not complited"); + TheLogger.log("Warning","Previous fetch is not complited"); } else { //TODO: Fetch timeout timer. Should be removed after #21 fix - _fetchingTimer = Timer(Duration(seconds: 10), () { + _fetchingTimer = Timer(Duration(seconds: 15), () { closeConnection(); _fetchCompleter.completeError({"errorCode" : 1,"errorMessage": "Connection timeout"}); }); @@ -73,10 +73,10 @@ class HassioDataModel { Future _reConnectSocket() { var _connectionCompleter = new Completer(); if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) { - debugPrint("Socket connecting..."); + TheLogger.log("Debug","Socket connecting..."); _hassioChannel = IOWebSocketChannel.connect(_hassioAPIEndpoint); _hassioChannel.stream.handleError((e) { - debugPrint("Unhandled socket error: ${e.toString()}"); + TheLogger.log("Error","Unhandled socket error: ${e.toString()}"); }); _hassioChannel.stream.listen((message) => _handleMessage(_connectionCompleter, message)); @@ -113,7 +113,7 @@ class HassioDataModel { _handleMessage(Completer connectionCompleter, String message) { var data = json.decode(message); - debugPrint("[Received]Message type: ${data['type']}"); + TheLogger.log("Debug","[Received] => Message type: ${data['type']}"); if (data["type"] == "auth_required") { _sendMessageRaw('{"type": "auth","$_hassioAuthType": "$_hassioPassword"}'); } else if (data["type"] == "auth_ok") { @@ -129,22 +129,18 @@ class HassioDataModel { } else if (data["id"] == _servicesMessageId) { _parseServices(data); } else if (data["id"] == _currentMessageId) { - debugPrint("Request id:$_currentMessageId was successful"); - } else { - debugPrint("Skipped message due to messageId:"); - debugPrint(message); + TheLogger.log("Debug","Request id:$_currentMessageId was successful"); } } else if (data["type"] == "event") { if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) { _handleEntityStateChange(data["event"]["data"]); } else if (data["event"] != null) { - debugPrint("Unhandled event type: ${data["event"]["event_type"]}"); + TheLogger.log("Warning","Unhandled event type: ${data["event"]["event_type"]}"); } else { - debugPrint("Event is null"); + TheLogger.log("Error","Event is null: $message"); } } else { - debugPrint("Unknown message type"); - debugPrint(message); + TheLogger.log("Warning","Unknown message type: $message"); } } @@ -185,18 +181,28 @@ class HassioDataModel { _currentMessageId += 1; } - _sendMessageRaw(message) { - debugPrint("[Sent]$message"); + _sendMessageRaw(String message) { + if (message.indexOf('"type": "auth"') > 0) { + TheLogger.log("Debug", "[Sending] ==> auth request"); + } else { + TheLogger.log("Debug", "[Sending] ==> $message"); + } _hassioChannel.sink.add(message); } void _handleEntityStateChange(Map eventData) { - String entityId = eventData["entity_id"]; - if (_entitiesData[entityId] != null) { - _entitiesData[entityId].addAll(eventData["new_state"]); - eventBus.fire(new StateChangedEvent(eventData["entity_id"])); + TheLogger.log("Debug", "Parsing new state for ${eventData['entity_id']}"); + if (eventData["new_state"] == null) { + TheLogger.log("Error", "No new_state found"); } else { - debugPrint("Unknown enity $entityId"); + var parsedEntityData = _parseEntity(eventData["new_state"]); + String entityId = parsedEntityData["entity_id"]; + if (_entitiesData[entityId] == null) { + _entitiesData[entityId] = parsedEntityData; + } else { + _entitiesData[entityId].addAll(parsedEntityData); + } + eventBus.fire(new StateChangedEvent(eventData["entity_id"])); } } @@ -214,109 +220,124 @@ class HassioDataModel { _servicesCompleter.completeError({"errorCode": 4, "errorMessage": response["error"]["message"]}); return; } - Map data = response["result"]; - Map result = {}; - debugPrint("Parsing ${data.length} Home Assistant service domains"); - data.forEach((domain, services){ - result[domain] = Map.from(services); - services.forEach((serviceName, serviceData){ - if (_entitiesData["$domain.$serviceName"] != null) { - result[domain].remove(serviceName); - } + try { + Map data = response["result"]; + Map result = {}; + TheLogger.log("Debug","Parsing ${data.length} Home Assistant service domains"); + data.forEach((domain, services) { + result[domain] = Map.from(services); + services.forEach((serviceName, serviceData) { + if (_entitiesData["$domain.$serviceName"] != null) { + result[domain].remove(serviceName); + } + }); }); - }); - _servicesData = result; - _servicesCompleter.complete(); + _servicesData = result; + _servicesCompleter.complete(); + } catch (e) { + //TODO hadle it properly + TheLogger.log("Error","Error parsing services. But they are not used :-)"); + _servicesCompleter.complete(); + } } void _parseEntities(response) async { + _entitiesData.clear(); + _uiStructure.clear(); if (response["success"] == false) { _statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]}); return; } List data = response["result"]; - debugPrint("Parsing ${data.length} Home Assistant entities"); + TheLogger.log("Debug","Parsing ${data.length} Home Assistant entities"); List uiGroups = []; data.forEach((entity) { - var composedEntity = Map.from(entity); - String entityDomain = entity["entity_id"].split(".")[0]; - String entityId = entity["entity_id"]; + try { + var composedEntity = _parseEntity(entity); - composedEntity["display_name"] = "${entity["attributes"]!=null ? entity["attributes"]["friendly_name"] ?? entity["attributes"]["name"] : "_"}"; - composedEntity["domain"] = entityDomain; - - if (composedEntity["attributes"] != null) { - if ((entityDomain == "group")&&(composedEntity["attributes"]["view"] == true)) { - uiGroups.add(entityId); + if (composedEntity["attributes"] != null) { + if ((composedEntity["domain"] == "group") && + (composedEntity["attributes"]["view"] == true)) { + uiGroups.add(composedEntity["entity_id"]); + } } + _entitiesData[entity["entity_id"]] = composedEntity; + } catch (error) { + TheLogger.log("Error","Error parsing entity: ${entity['entity_id']}"); } - - - if (entityDomain == "group") { - if ((composedEntity["attributes"] != null) && - (composedEntity["attributes"]["view"] == true)) { - - } - } - - _entitiesData[entityId] = Map.from(composedEntity); }); //Gethering information for UI - debugPrint("Gethering views"); + TheLogger.log("Debug","Gethering views"); int viewCounter = 0; uiGroups.forEach((viewId) { //Each view - viewCounter +=1; - var viewGroup = _entitiesData[viewId]; - Map viewGroupStructure = {}; - if (viewGroup != null) { - viewGroupStructure["groups"] = {}; - viewGroupStructure["state"] = "on"; - viewGroupStructure["entity_id"] = viewGroup["entity_id"]; - viewGroupStructure["badges"] = {"children": []}; - viewGroupStructure["attributes"] = viewGroup["attributes"] != null ? {"icon": viewGroup["attributes"]["icon"]} : {"icon": "none"}; + try { + Map viewGroupStructure = {}; + viewCounter += 1; + var viewGroup = _entitiesData[viewId]; + if (viewGroup != null) { + viewGroupStructure["groups"] = {}; + viewGroupStructure["state"] = "on"; + viewGroupStructure["entity_id"] = viewGroup["entity_id"]; + viewGroupStructure["badges"] = {"children": []}; + viewGroupStructure["attributes"] = viewGroup["attributes"] != null ? { + "icon": viewGroup["attributes"]["icon"] + } : {"icon": "none"}; - viewGroup["attributes"]["entity_id"].forEach((entityId) { //Each entity or group in view - Map newGroup = {}; - String domain = _entitiesData[entityId]["domain"]; - if (domain != "group") { - if (_topBadgeDomains.contains(domain)) { - viewGroupStructure["badges"]["children"].add(entityId); - } else { - String autoGroupID = "$domain.$domain$viewCounter"; - if (viewGroupStructure["groups"]["$autoGroupID"] == null) { - newGroup["entity_id"] = "$domain.$domain$viewCounter"; - newGroup["friendly_name"] = "$domain"; - newGroup["children"] = []; - newGroup["children"].add(entityId); - viewGroupStructure["groups"]["$autoGroupID"] = - Map.from(newGroup); + viewGroup["attributes"]["entity_id"].forEach(( + entityId) { //Each entity or group in view + Map newGroup = {}; + String domain = _entitiesData[entityId]["domain"]; + if (domain != "group") { + if (_topBadgeDomains.contains(domain)) { + viewGroupStructure["badges"]["children"].add(entityId); } else { - viewGroupStructure["groups"]["$autoGroupID"]["children"].add( - entityId); + String autoGroupID = "$domain.$domain$viewCounter"; + if (viewGroupStructure["groups"]["$autoGroupID"] == null) { + newGroup["entity_id"] = "$domain.$domain$viewCounter"; + newGroup["friendly_name"] = "$domain"; + newGroup["children"] = []; + newGroup["children"].add(entityId); + viewGroupStructure["groups"]["$autoGroupID"] = + Map.from(newGroup); + } else { + viewGroupStructure["groups"]["$autoGroupID"]["children"].add( + entityId); + } } + } else { + newGroup["entity_id"] = entityId; + newGroup["friendly_name"] = + (_entitiesData[entityId]['attributes'] != null) + ? (_entitiesData[entityId]['attributes']['friendly_name'] ?? + "") + : ""; + newGroup["children"] = List(); + _entitiesData[entityId]["attributes"]["entity_id"].forEach(( + groupedEntityId) { + newGroup["children"].add(groupedEntityId); + }); + viewGroupStructure["groups"]["$entityId"] = Map.from(newGroup); } - } else { - newGroup["entity_id"] = entityId; - newGroup["friendly_name"] = - (_entitiesData[entityId]['attributes'] != null) - ? (_entitiesData[entityId]['attributes']['friendly_name'] ?? "") - : ""; - newGroup["children"] = List(); - _entitiesData[entityId]["attributes"]["entity_id"].forEach(( - groupedEntityId) { - newGroup["children"].add(groupedEntityId); - }); - viewGroupStructure["groups"]["$entityId"] = Map.from(newGroup); - } - }); - _uiStructure[viewId.split(".")[1]] = viewGroupStructure; + }); + } + _uiStructure[viewId.split(".")[1]] = viewGroupStructure; + } catch (error) { + TheLogger.log("Error","Error parsing view: $viewId"); } }); _statesCompleter.complete(); } + Map _parseEntity(rawData) { + var composedEntity = Map.from(rawData); + String entityDomain = rawData["entity_id"].split(".")[0]; + composedEntity["display_name"] = "${rawData["attributes"]!=null ? rawData["attributes"]["friendly_name"] ?? rawData["attributes"]["name"] : "_"}"; + composedEntity["domain"] = entityDomain; + return composedEntity; + } + Future callService(String domain, String service, String entity_id) { var sendCompleter = Completer(); //TODO: Send service call timeout timer. Should be removed after #21 fix diff --git a/lib/logPage.dart b/lib/logPage.dart new file mode 100644 index 0000000..9bf7fd2 --- /dev/null +++ b/lib/logPage.dart @@ -0,0 +1,49 @@ +part of 'main.dart'; + +class LogViewPage extends StatefulWidget { + LogViewPage({Key key, this.title}) : super(key: key); + + final String title; + + @override + _LogViewPageState createState() => new _LogViewPageState(); +} + +class _LogViewPageState extends State { + String _hassioDomain = ""; + String _hassioPort = "8123"; + String _hassioPassword = ""; + String _socketProtocol = "wss"; + String _authType = "access_token"; + + @override + void initState() { + super.initState(); + _loadLog(); + } + + _loadLog() async { + // + } + + @override + Widget build(BuildContext context) { + return new Scaffold( + appBar: new AppBar( + leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){ + Navigator.pop(context); + }), + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + title: new Text(widget.title), + ), + body: TextField( + maxLines: null, + + controller: TextEditingController( + text: TheLogger.getLog() + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 0ec374a..d04fe9c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,15 +9,46 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/widgets.dart'; import 'package:cached_network_image/cached_network_image.dart'; -part 'settings.dart'; +part 'settingsPage.dart'; part 'data_model.dart'; +part 'logPage.dart'; EventBus eventBus = new EventBus(); const String appName = "HA Client"; -const appVersion = "0.1.0-alpha"; +const appVersion = "0.1.1-alpha"; String homeAssistantWebHost; +class TheLogger { + + static List _log = []; + + static String getLog() { + String res = ''; + _log.forEach((line) { + res += "$line\n\n"; + }); + return res; + } + + static bool get isInDebugMode { + bool inDebugMode = false; + + assert(inDebugMode = true); + + return inDebugMode; + } + + static void log(String level, String message) { + debugPrint('$message'); + _log.add("[$level] : $message"); + if (_log.length > 50) { + _log.removeAt(0); + } + } + +} + void main() => runApp(new HassClientApp()); class HassClientApp extends StatelessWidget { @@ -32,7 +63,8 @@ class HassClientApp extends StatelessWidget { initialRoute: "/", routes: { "/": (context) => MainPage(title: 'Hass Client'), - "/connection-settings": (context) => ConnectionSettingsPage(title: "Connection Settings") + "/connection-settings": (context) => ConnectionSettingsPage(title: "Connection Settings"), + "/log-view": (context) => LogViewPage(title: "Log") }, ); } @@ -76,7 +108,7 @@ class _MainPageState extends State with WidgetsBindingObserver { super.initState(); WidgetsBinding.instance.addObserver(this); _settingsSubscription = eventBus.on().listen((event) { - debugPrint("Settings change event: reconnect=${event.reconnect}"); + TheLogger.log("Debug","Settings change event: reconnect=${event.reconnect}"); setState(() { _errorCodeToBeShown = 0; }); @@ -87,7 +119,7 @@ class _MainPageState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { - debugPrint("$state"); + TheLogger.log("Debug","$state"); if (state == AppLifecycleState.resumed) { _refreshData(); } @@ -118,7 +150,6 @@ class _MainPageState extends State with WidgetsBindingObserver { _refreshData(); if (_stateSubscription != null) _stateSubscription.cancel(); _stateSubscription = eventBus.on().listen((event) { - debugPrint("State change event for ${event.entityId}"); setState(() { _entitiesData = _dataModel.entities; }); @@ -212,11 +243,9 @@ class _MainPageState extends State with WidgetsBindingObserver { List result = []; ids.forEach((entityId) { var data = _entitiesData[entityId]; - if (data == null) { - debugPrint("Hiding unknown entity from badges: $entityId"); - } else { + if (data != null) { result.add( - _buildSingleBadge(data) + _buildSingleBadge(data) ); } }); @@ -363,9 +392,7 @@ class _MainPageState extends State with WidgetsBindingObserver { List entities = []; ids.forEach((id) { var data = _entitiesData[id]; - if (data == null) { - debugPrint("Hiding unknown entity from card: $id"); - } else { + if (data != null) { entities.add(new ListTile( leading: MaterialDesignIcons.createIconFromEntityData(data, 28.0, _stateIconColors[data["state"]] ?? Colors.blueGrey), //subtitle: Text("${data['entity_id']}"), @@ -487,6 +514,13 @@ class _MainPageState extends State with WidgetsBindingObserver { Navigator.pushNamed(context, '/connection-settings'); }, ), + new ListTile( + leading: Icon(Icons.insert_drive_file), + title: Text("Log"), + onTap: () { + Navigator.pushNamed(context, '/log-view'); + }, + ), new AboutListTile( applicationName: appName, applicationVersion: appVersion, diff --git a/lib/settings.dart b/lib/settingsPage.dart similarity index 96% rename from lib/settings.dart rename to lib/settingsPage.dart index db02d27..60cb8ab 100644 --- a/lib/settings.dart +++ b/lib/settingsPage.dart @@ -35,6 +35,9 @@ class _ConnectionSettingsPageState extends State { } _saveSettings() async { + if (_hassioDomain.indexOf("http") == 0 && _hassioDomain.indexOf("//") > 0) { + _hassioDomain = _hassioDomain.split("//")[1]; + } SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.setString("hassio-domain", _hassioDomain); prefs.setString("hassio-port", _hassioPort); diff --git a/pubspec.yaml b/pubspec.yaml index 9922c8d..ec820c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: hass_client description: Home Assistant Android Client -version: 0.1.0-alpha +version: 0.1.1-alpha environment: sdk: ">=2.0.0-dev.68.0 <3.0.0"