From 4dc211f2f7479192b62e291cb254ede65bcc21b4 Mon Sep 17 00:00:00 2001 From: estevez Date: Sun, 23 Sep 2018 18:56:54 +0300 Subject: [PATCH 1/5] [#51,#35] Proper handling new entities appeared after refresh --- lib/data_model.dart | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/lib/data_model.dart b/lib/data_model.dart index 51c9166..1493ae1 100644 --- a/lib/data_model.dart +++ b/lib/data_model.dart @@ -191,13 +191,14 @@ class HassioDataModel { } 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"])); + var parsedEntityData = _parseEntity(eventData["new_state"]); + String entityId = parsedEntityData["entity_id"]; + if (_entitiesData[entityId] == null) { + _entitiesData[entityId] = parsedEntityData; } else { - debugPrint("Unknown enity $entityId"); + _entitiesData[entityId].addAll(parsedEntityData); } + eventBus.fire(new StateChangedEvent(eventData["entity_id"])); } void _parseConfig(Map data) { @@ -230,6 +231,8 @@ class HassioDataModel { } void _parseEntities(response) async { + _entitiesData.clear(); + _uiStructure.clear(); if (response["success"] == false) { _statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]}); return; @@ -238,28 +241,15 @@ class HassioDataModel { debugPrint("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"]; - - composedEntity["display_name"] = "${entity["attributes"]!=null ? entity["attributes"]["friendly_name"] ?? entity["attributes"]["name"] : "_"}"; - composedEntity["domain"] = entityDomain; + var composedEntity = _parseEntity(entity); if (composedEntity["attributes"] != null) { - if ((entityDomain == "group")&&(composedEntity["attributes"]["view"] == true)) { - uiGroups.add(entityId); + if ((composedEntity["domain"] == "group")&&(composedEntity["attributes"]["view"] == true)) { + uiGroups.add(composedEntity["entity_id"]); } } - - if (entityDomain == "group") { - if ((composedEntity["attributes"] != null) && - (composedEntity["attributes"]["view"] == true)) { - - } - } - - _entitiesData[entityId] = Map.from(composedEntity); + _entitiesData[composedEntity["entity_id"]] = composedEntity; }); //Gethering information for UI @@ -317,6 +307,14 @@ class HassioDataModel { _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 From 86738a05159a02f70d51d6ead51e687e6db64ab3 Mon Sep 17 00:00:00 2001 From: estevez Date: Sun, 23 Sep 2018 22:20:48 +0300 Subject: [PATCH 2/5] Skiping entity and views that cause parsing errors --- lib/data_model.dart | 139 +++++++++++++++++++++++++------------------- 1 file changed, 79 insertions(+), 60 deletions(-) diff --git a/lib/data_model.dart b/lib/data_model.dart index 1493ae1..ca166d0 100644 --- a/lib/data_model.dart +++ b/lib/data_model.dart @@ -49,7 +49,7 @@ class HassioDataModel { debugPrint("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"}); }); @@ -215,19 +215,25 @@ 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 = {}; + 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); + } + }); }); - }); - _servicesData = result; - _servicesCompleter.complete(); + _servicesData = result; + _servicesCompleter.complete(); + } catch (e) { + //TODO hadle it properly + debugPrint("Error parsing services"); + _servicesCompleter.complete(); + } } void _parseEntities(response) async { @@ -241,67 +247,80 @@ class HassioDataModel { debugPrint("Parsing ${data.length} Home Assistant entities"); List uiGroups = []; data.forEach((entity) { - var composedEntity = _parseEntity(entity); + try { + var composedEntity = _parseEntity(entity); - if (composedEntity["attributes"] != null) { - if ((composedEntity["domain"] == "group")&&(composedEntity["attributes"]["view"] == true)) { - uiGroups.add(composedEntity["entity_id"]); + if (composedEntity["attributes"] != null) { + if ((composedEntity["domain"] == "group") && + (composedEntity["attributes"]["view"] == true)) { + uiGroups.add(composedEntity["entity_id"]); + } } + _entitiesData[entity["entity_id"]] = composedEntity; + } catch (error) { + debugPrint("Error parsing entity: ${entity['entity_id']}"); + debugPrint("$error"); } - - _entitiesData[composedEntity["entity_id"]] = composedEntity; }); //Gethering information for UI debugPrint("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) { + debugPrint("Error parsing view: $viewId"); } }); _statesCompleter.complete(); From 7cad0141c72d238d98c951469f659d662fbfcf6c Mon Sep 17 00:00:00 2001 From: estevez Date: Sun, 23 Sep 2018 22:24:48 +0300 Subject: [PATCH 3/5] Remove protocol from domain field on saving settings --- lib/settings.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/settings.dart b/lib/settings.dart index db02d27..60cb8ab 100644 --- a/lib/settings.dart +++ b/lib/settings.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); From 36d727b454ab27a8c47d4db94f07f4f2550a041b Mon Sep 17 00:00:00 2001 From: estevez Date: Mon, 24 Sep 2018 00:05:57 +0300 Subject: [PATCH 4/5] Add log view page to see app events and debug messages --- lib/data_model.dart | 58 +++++++++++++----------- lib/logPage.dart | 49 ++++++++++++++++++++ lib/main.dart | 58 +++++++++++++++++++----- lib/{settings.dart => settingsPage.dart} | 0 4 files changed, 126 insertions(+), 39 deletions(-) create mode 100644 lib/logPage.dart rename lib/{settings.dart => settingsPage.dart} (100%) diff --git a/lib/data_model.dart b/lib/data_model.dart index ca166d0..c46505f 100644 --- a/lib/data_model.dart +++ b/lib/data_model.dart @@ -46,7 +46,7 @@ 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: 15), () { @@ -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,20 +181,29 @@ 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) { - var parsedEntityData = _parseEntity(eventData["new_state"]); - String entityId = parsedEntityData["entity_id"]; - if (_entitiesData[entityId] == null) { - _entitiesData[entityId] = parsedEntityData; + TheLogger.log("Debug", "Parsing new state for ${eventData['entity_id']}"); + if (eventData["new_state"] == null) { + TheLogger.log("Error", "No new_state found"); } else { - _entitiesData[entityId].addAll(parsedEntityData); + 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"])); } - eventBus.fire(new StateChangedEvent(eventData["entity_id"])); } void _parseConfig(Map data) { @@ -218,7 +223,7 @@ class HassioDataModel { try { Map data = response["result"]; Map result = {}; - debugPrint("Parsing ${data.length} Home Assistant service domains"); + TheLogger.log("Debug","Parsing ${data.length} Home Assistant service domains"); data.forEach((domain, services) { result[domain] = Map.from(services); services.forEach((serviceName, serviceData) { @@ -231,7 +236,7 @@ class HassioDataModel { _servicesCompleter.complete(); } catch (e) { //TODO hadle it properly - debugPrint("Error parsing services"); + TheLogger.log("Error","Error parsing services. But they are not used :-)"); _servicesCompleter.complete(); } } @@ -244,7 +249,7 @@ class HassioDataModel { 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) { try { @@ -258,13 +263,12 @@ class HassioDataModel { } _entitiesData[entity["entity_id"]] = composedEntity; } catch (error) { - debugPrint("Error parsing entity: ${entity['entity_id']}"); - debugPrint("$error"); + TheLogger.log("Error","Error parsing entity: ${entity['entity_id']}"); } }); //Gethering information for UI - debugPrint("Gethering views"); + TheLogger.log("Debug","Gethering views"); int viewCounter = 0; uiGroups.forEach((viewId) { //Each view try { @@ -320,7 +324,7 @@ class HassioDataModel { } _uiStructure[viewId.split(".")[1]] = viewGroupStructure; } catch (error) { - debugPrint("Error parsing view: $viewId"); + TheLogger.log("Error","Error parsing view: $viewId"); } }); _statesCompleter.complete(); 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..057636e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,8 +9,9 @@ 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"; @@ -18,6 +19,36 @@ const appVersion = "0.1.0-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 100% rename from lib/settings.dart rename to lib/settingsPage.dart From aa0d7ee8fd1270868030b1053150a6b4fb8f4f2e Mon Sep 17 00:00:00 2001 From: estevez Date: Mon, 24 Sep 2018 00:07:38 +0300 Subject: [PATCH 5/5] version 0.1.1 --- android/app/build.gradle | 4 ++-- lib/main.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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/main.dart b/lib/main.dart index 057636e..d04fe9c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,7 +15,7 @@ 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; 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"