From cd17eabb63057a3fe5c1f20628e70000d7825b9e Mon Sep 17 00:00:00 2001 From: estevez Date: Sat, 15 Sep 2018 01:46:15 +0300 Subject: [PATCH] [#10][#16][#4][#20] A lot about connection and getting data. --- lib/data_model.dart | 193 +++++++++++++++++++++++++++++++++++++++ lib/main.dart | 216 ++++++++++++++++---------------------------- pubspec.lock | 7 ++ pubspec.yaml | 1 + 4 files changed, 278 insertions(+), 139 deletions(-) create mode 100644 lib/data_model.dart diff --git a/lib/data_model.dart b/lib/data_model.dart new file mode 100644 index 0000000..588febf --- /dev/null +++ b/lib/data_model.dart @@ -0,0 +1,193 @@ +part of 'main.dart'; + +class HassioDataModel { + String _hassioAPIEndpoint; + String _hassioPassword; + IOWebSocketChannel _hassioChannel; + int _currentMssageId = 0; + int _statesMessageId = 0; + int _servicesMessageId = 0; + Map _entitiesData = {}; + Map _servicesData = {}; + Map _uiStructure = {}; + Completer _fetchCompleter; + Completer _statesCompleter; + Completer _servicesCompleter; + + Map get entities => _entitiesData; + Map get services => _servicesData; + Map get uiStructure => _uiStructure; + + HassioDataModel(String url, String password) { + _hassioAPIEndpoint = url; + _hassioPassword = password; + } + + Future fetch() { + _fetchCompleter = new Completer(); + _reConnectSocket().then((r) { + _getData(); + }).catchError((e){ + _fetchCompleter.completeError(e); + }); + return _fetchCompleter.future; + } + + Future _reConnectSocket() { + var _connectionCompleter = new Completer(); + if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) { + debugPrint("Socket connecting..."); + _hassioChannel = IOWebSocketChannel.connect(_hassioAPIEndpoint); + _hassioChannel.stream.handleError((e) { + debugPrint("Socket error: ${e.toString()}"); + }); + _hassioChannel.stream.listen((message) => + _handleMessage(_connectionCompleter, message)); + } else { + _connectionCompleter.complete(); + } + return _connectionCompleter.future; + } + + _getData() { + _getStates().then((result) { + _getServices().then((result) { + _fetchCompleter.complete(); + }).catchError((e) { + _fetchCompleter.completeError(e); + }); + }).catchError((e) { + _fetchCompleter.completeError(e); + }); + + + } + + _handleMessage(Completer connectionCompleter, String message) { + debugPrint("<[Receive]Message from Home Assistant:"); + var data = json.decode(message); + debugPrint(" type: ${data['type']}"); + if (data["type"] == "auth_required") { + debugPrint(" sending auth!"); + _sendMessageRaw('{"type": "auth","api_password": "$_hassioPassword"}'); + } else if (data["type"] == "auth_ok") { + debugPrint(" auth done"); + debugPrint("Connection done"); + connectionCompleter.complete(); + } else if (data["type"] == "auth_invalid") { + connectionCompleter.completeError({message: "Auth error: ${data["message"]}"}); + } else if (data["type"] == "result") { + if (data["success"] == true) { + if (data["id"] == _statesMessageId) { + _parseEntities(data["result"]); + _statesCompleter.complete(); + } else if (data["id"] == _servicesMessageId) { + _parseServices(data["result"]); + _servicesCompleter.complete(); + } else if (data["id"] == _currentMssageId) { + debugPrint("Request id:$_currentMssageId was successful"); + } else { + _handleErrorMessage({"message" : "Wrong message ID"}); + } + } else { + _handleErrorMessage(data["error"]); + } + } + } + + _handleErrorMessage(Object error) { + debugPrint("Error: ${error.toString()}"); + if (!_statesCompleter.isCompleted) _statesCompleter.completeError(error); + if (!_servicesCompleter.isCompleted) _servicesCompleter.completeError(error); + } + + Future _getStates() { + _statesCompleter = new Completer(); + _incrementMessageId(); + _statesMessageId = _currentMssageId; + _sendMessageRaw('{"id": $_currentMssageId, "type": "get_states"}'); + + return _statesCompleter.future; + } + + Future _getServices() { + _servicesCompleter = new Completer(); + _incrementMessageId(); + _servicesMessageId = _currentMssageId; + _sendMessageRaw('{"id": $_currentMssageId, "type": "get_services"}'); + + return _servicesCompleter.future; + } + + _incrementMessageId() { + _currentMssageId += 1; + } + + _sendMessageRaw(message) { + _reConnectSocket().then((r) { + debugPrint(">[Send]Sending to Home Assistant:"); + debugPrint(" $message"); + _hassioChannel.sink.add(message); + }).catchError((e){ + debugPrint("Unable to connect for sending =("); + }); + + + } + + void _parseServices(Map data) { + 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; + } + + void _parseEntities(List data) async { + Map switchServices = { + "turn_on": {}, + "turn_off": {}, + "toggle": {} + }; + debugPrint("Parsing ${data.length} Home Assistant entities"); + 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; + + if ((entityDomain == "automation") || (entityDomain == "light") || (entityDomain == "switch") || (entityDomain == "script")) { + composedEntity["services"] = Map.from(switchServices); + } + + _entitiesData[entityId] = Map.from(composedEntity); + }); + var defaultView = _entitiesData["group.default_view"]; + debugPrint("Gethering default view"); + if (defaultView!= null) { + defaultView["attributes"]["entity_id"].forEach((entityId) { + if (_entitiesData[entityId]["domain"] != "group") { + _uiStructure[entityId] = _entitiesData[entityId]; + } else { + _entitiesData[entityId]["attributes"]["entity_id"].forEach((groupedEntityId) { + _uiStructure[groupedEntityId] = _entitiesData[groupedEntityId]; + }); + } + }); + } + + } + + callService(String domain, String service, String entity_id) { + _incrementMessageId(); + _sendMessageRaw('{"id": $_currentMssageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entity_id"}}'); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 271d20c..1ab43e4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,14 @@ import 'dart:convert'; +import 'dart:async'; import 'package:http/http.dart' as http; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:web_socket_channel/io.dart'; import 'package:web_socket_channel/status.dart' as socketStatus; +import 'package:progress_indicators/progress_indicators.dart'; part 'settings.dart'; +part 'data_model.dart'; void main() => runApp(new HassClientApp()); @@ -30,15 +33,6 @@ class HassClientApp extends StatelessWidget { class MainPage extends StatefulWidget { MainPage({Key key, this.title}) : super(key: key); - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - final String title; @override @@ -46,14 +40,11 @@ class MainPage extends StatefulWidget { } class _MainPageState extends State { - Map _entitiesData = {}; - Map _servicesData = {}; - String _hassioAPIEndpoint = ""; - String _hassioPassword = ""; - IOWebSocketChannel _hassioChannel; - int _entitiesMessageId = 0; - int _servicesMessageId = 1; - int _servicCallMessageId = 2; + + HassioDataModel _dataModel; + Map _entitiesData; + String _dataModelErrorMessage = ""; + bool loading = true; @override void initState() { @@ -63,115 +54,30 @@ class _MainPageState extends State { _initClient() async { SharedPreferences prefs = await SharedPreferences.getInstance(); + String _hassioAPIEndpoint = "wss://" + prefs.getString('hassio-domain') +":" + prefs.getString('hassio-port') + "/api/websocket"; + String _hassioPassword = prefs.getString('hassio-password'); + _dataModel = HassioDataModel(_hassioAPIEndpoint, _hassioPassword); + await _refreshData(); + } + + _refreshData() async { setState(() { - _hassioAPIEndpoint = "wss://" + prefs.getString('hassio-domain') +":" + prefs.getString('hassio-port') + "/api/websocket"; - _hassioPassword = prefs.getString('hassio-password'); + loading = true; }); - _connectSocket(); - } - - _connectSocket() async { - debugPrint("Socket connecting..."); - _hassioChannel = await IOWebSocketChannel.connect(_hassioAPIEndpoint); - _hassioChannel.stream.listen((message) { - _handleSocketMessage(message); - }); - debugPrint("Socket connected!"); - - } - - _handleSocketMessage(message) { - debugPrint("<== Message from Home Assistant:"); - debugPrint(message); - var data = json.decode(message); - if (data["type"] == "auth_required") { - _sendHassioAuth(); - } else if (data["type"] == "auth_ok") { - debugPrint("Auth done!"); - _startDataFetching(); - } else if (data["type"] == "result") { - if (data["success"] == true) { - if (data["id"] == _entitiesMessageId) { - _loadEntities(data["result"]); - _sendRawMessage('{"id": $_servicesMessageId, "type": "get_services"}'); - } else if (data["id"] == _servicesMessageId) { - _loadServices(data["result"]); - } - } else { - /* - Handle error here - */ - } - } - } - - _incrementMessageId() { - _entitiesMessageId = _servicCallMessageId + 1; - _servicesMessageId = _entitiesMessageId + 1; - _servicCallMessageId = _servicesMessageId + 1; - } - - _sendHassioAuth() { - _sendRawMessage('{"type": "auth","api_password": "$_hassioPassword"}'); - } - - _startDataFetching() { - _incrementMessageId(); - _sendRawMessage('{"id": $_entitiesMessageId, "type": "get_states"}'); - } - - _sendRawMessage(message) { - if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) { - debugPrint("Socket is closed"); - } - debugPrint("==> Sending to Home Assistant:"); - debugPrint(message); - _hassioChannel.sink.add(message); - } - - _sendServiceCall(String domain, String service, String entityId) { - _incrementMessageId(); - _sendRawMessage('{"id": $_servicCallMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"}}'); - } - - void _loadServices(Map data) { - Map result = {}; - data.forEach((domain, services){ - result[domain] = Map.from(services); - services.forEach((serviceName, serviceData){ - if (_entitiesData["$domain.$serviceName"] != null) { - result[domain].remove(serviceName); - } + _dataModelErrorMessage = ""; + if (_dataModel != null) { + await _dataModel.fetch().then((result) { + setState(() { + _entitiesData = _dataModel._uiStructure; + loading = false; + }); + }).catchError((e) { + setState(() { + _dataModelErrorMessage = e.toString(); + loading = false; + }); }); - }); - setState(() { - _servicesData = result; - }); - } - - void _loadEntities(List data) { - Map switchServices = { - "turn_on": {}, - "turn_off": {}, - "toggle": {} - }; - debugPrint("Getting Home Assistant entities: ${data.length}"); - 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; - - if ((entityDomain == "automation") || (entityDomain == "light") || (entityDomain == "switch") || (entityDomain == "script")) { - composedEntity["services"] = Map.from(switchServices); - } - - setState(() { - _entitiesData[entityId] = Map.from(composedEntity); - }); - }); + } } Widget buildEntityButtons(String entityId) { @@ -181,9 +87,9 @@ class _MainPageState extends State { List buttons = []; _entitiesData[entityId]["services"].forEach((key, value) { buttons.add(new FlatButton( - child: Text(_entitiesData[entityId]["domain"] + ".$key"), + child: Text('$key'), onPressed: () { - _sendServiceCall(_entitiesData[entityId]["domain"], key, _entitiesData[entityId]["entity_id"]); + _dataModel.callService(_entitiesData[entityId]["domain"], key, _entitiesData[entityId]["entity_id"]); }, )); }); @@ -192,19 +98,19 @@ class _MainPageState extends State { ); } - Widget buildEntityCard(String entityId) { + Widget buildEntityCard(data) { return Card( child: new Column( mainAxisSize: MainAxisSize.min, children: [ new ListTile( leading: const Icon(Icons.device_hub), - subtitle: Text("$entityId"), - trailing: Text("${_entitiesData[entityId]["state"]}"), - title: Text("${_entitiesData[entityId]["display_name"]}"), + subtitle: Text("${data['entity_id']}"), + trailing: Text("${data["state"]}"), + title: Text("${data["display_name"]}"), ), new ButtonTheme.bar( // make buttons use the appropriate styles for cards - child: buildEntityButtons(entityId), + child: buildEntityButtons(data['entity_id']), ), ], ), @@ -212,11 +118,42 @@ class _MainPageState extends State { } List buildEntitiesView() { - List result = []; - _entitiesData.forEach((key, data){ - result.add(buildEntityCard(key)); - }); - return result; + if (_entitiesData != null) { + List result = []; + if (_dataModelErrorMessage.length == 0) { + _entitiesData.forEach((key, data) { + if (data != null) { + result.add(buildEntityCard(data)); + } else { + debugPrint("Unknown entity: $key"); + } + }); + } else { + result.add(Text(_dataModelErrorMessage)); + } + return result; + } else { + return [Container(width: 0.0, height: 0.0)]; + } + } + + Widget _buildTitle() { + Row titleRow = Row( + children: [ + Text(widget.title) + ], + ); + if (loading) { + titleRow.children.add(Padding( + child: JumpingDotsProgressIndicator( + fontSize: 30.0, + color: Colors.white, + ), + padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 40.0), + ) + ); + } + return titleRow; } @override @@ -231,7 +168,7 @@ class _MainPageState extends State { appBar: new AppBar( // 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), + title: _buildTitle(), ), drawer: new Drawer( child: ListView( @@ -262,16 +199,17 @@ class _MainPageState extends State { children: buildEntitiesView(), ), floatingActionButton: new FloatingActionButton( - onPressed: _startDataFetching, + onPressed: _refreshData, tooltip: 'Increment', child: new Icon(Icons.refresh), - ), // This trailing comma makes auto-formatting nicer for build methods. + ), ); } @override void dispose() { - _hassioChannel.sink.close(); + //TODO + //_hassioChannel.sink.close(); super.dispose(); } } diff --git a/pubspec.lock b/pubspec.lock index 15d4bbc..2681079 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -228,6 +228,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.6" + progress_indicators: + dependency: "direct main" + description: + name: progress_indicators + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 12011f8..09bf3df 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: flutter: sdk: flutter shared_preferences: any + progress_indicators: ^0.1.2 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.