import 'dart:convert'; import 'dart:async'; import 'package:flutter/rendering.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:web_socket_channel/io.dart'; import 'package:progress_indicators/progress_indicators.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/widgets.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter/services.dart'; import 'package:date_format/date_format.dart'; part 'entity_class/entity.class.dart'; part 'entity_class/button_entity.class.dart'; part 'entity_class/datetime_entity.class.dart'; part 'entity_class/select_entity.class.dart'; part 'entity_class/slider_entity.class.dart'; part 'entity_class/switch_entity.class.dart'; part 'entity_class/text_entity.class.dart'; part 'settings.page.dart'; part 'home_assistant.class.dart'; part 'log.page.dart'; part 'entity.page.dart'; part 'utils.class.dart'; part 'mdi.class.dart'; part 'entity_collection.class.dart'; part 'view_builder.class.dart'; part 'view_class.dart'; part 'card_class.dart'; EventBus eventBus = new EventBus(); const String appName = "HA Client"; const appVersion = "0.2.4"; String homeAssistantWebHost; void main() { FlutterError.onError = (errorDetails) { TheLogger.log("Error", "${errorDetails.exception}"); if (TheLogger.isInDebugMode) { FlutterError.dumpErrorToConsole(errorDetails); } }; runZoned(() { runApp(new HAClientApp()); }, onError: (error, stack) { TheLogger.log("Global error", "$error"); if (TheLogger.isInDebugMode) { debugPrint("$stack"); } }); } class HAClientApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return new MaterialApp( title: appName, theme: new ThemeData( primarySwatch: Colors.blue, ), initialRoute: "/", routes: { "/": (context) => MainPage(title: 'HA Client'), "/connection-settings": (context) => ConnectionSettingsPage(title: "Connection Settings"), "/log-view": (context) => LogViewPage(title: "Log") }, ); } } class MainPage extends StatefulWidget { MainPage({Key key, this.title}) : super(key: key); final String title; @override _MainPageState createState() => new _MainPageState(); } class _MainPageState extends State with WidgetsBindingObserver { HomeAssistant _homeAssistant; EntityCollection _entities; //Map _instanceConfig; String _apiEndpoint; String _apiPassword; String _authType; int _uiViewsCount = 0; String _instanceHost; int _errorCodeToBeShown = 0; String _lastErrorMessage = ""; StreamSubscription _stateSubscription; StreamSubscription _settingsSubscription; StreamSubscription _serviceCallSubscription; StreamSubscription _showEntityPageSubscription; StreamSubscription _refreshDataSubscription; bool _isLoading = true; bool _settingsLoaded = false; @override void initState() { super.initState(); _settingsLoaded = false; WidgetsBinding.instance.addObserver(this); _homeAssistant = HomeAssistant(); _settingsSubscription = eventBus.on().listen((event) { TheLogger.log("Debug","Settings change event: reconnect=${event.reconnect}"); if (event.reconnect) { _homeAssistant.disconnect().then((_){ _loadConnectionSettings().then((b){ _refreshData(); }, onError: (_) { setState(() { _lastErrorMessage = _; _errorCodeToBeShown = 5; }); } ); }); } }); _loadConnectionSettings().then((_){ _createConnection(); }, onError: (_) { setState(() { _lastErrorMessage = _; _errorCodeToBeShown = 5; }); }); } @override void didChangeAppLifecycleState(AppLifecycleState state) { TheLogger.log("Debug","$state"); if (state == AppLifecycleState.resumed && _settingsLoaded) { _refreshData(); } } _loadConnectionSettings() async { SharedPreferences prefs = await SharedPreferences.getInstance(); String domain = prefs.getString('hassio-domain'); String port = prefs.getString('hassio-port'); _instanceHost = "$domain:$port"; _apiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket"; homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port"; _apiPassword = prefs.getString('hassio-password'); _authType = prefs.getString('hassio-auth-type'); if ((domain == null) || (port == null) || (_apiPassword == null) || (domain.length == 0) || (port.length == 0) || (_apiPassword.length == 0)) { throw("Check connection settings"); } else { _settingsLoaded = true; } } _createConnection() { _refreshData(); if (_stateSubscription == null) { _stateSubscription = eventBus.on().listen((event) { setState(() { if (event.localChange) { _entities .get(event.entityId) .state = event.newState; } }); }); } if (_serviceCallSubscription == null) { _serviceCallSubscription = eventBus.on().listen((event) { _callService(event.domain, event.service, event.entityId, event.additionalParams); }); } if (_showEntityPageSubscription == null) { _showEntityPageSubscription = eventBus.on().listen((event) { _showEntityPage(event.entity); }); } if (_refreshDataSubscription == null) { _refreshDataSubscription = eventBus.on().listen((event){ _refreshData(); }); } } _refreshData() async { _homeAssistant.updateConnectionSettings(_apiEndpoint, _apiPassword, _authType); setState(() { _isLoading = true; }); _errorCodeToBeShown = 0; _lastErrorMessage = ""; await _homeAssistant.fetch().then((result) { setState(() { //_instanceConfig = _homeAssistant.instanceConfig; _entities = _homeAssistant.entities; _uiViewsCount = _homeAssistant.viewsCount; _isLoading = false; }); }).catchError((e) { _setErrorState(e); }); eventBus.fire(RefreshDataFinishedEvent()); } _setErrorState(e) { setState(() { _errorCodeToBeShown = e["errorCode"] != null ? e["errorCode"] : 99; _lastErrorMessage = e["errorMessage"] ?? "Unknown error"; _isLoading = false; }); } void _callService(String domain, String service, String entityId, Map additionalParams) { _homeAssistant.callService(domain, service, entityId, additionalParams).catchError((e) => _setErrorState(e)); } void _showEntityPage(Entity entity) { Navigator.push( context, MaterialPageRoute( builder: (context) => EntityViewPage(entity: entity), ) ); } List buildUIViewTabs() { //TODO move somewhere to ViewBuilder List result = []; if (!_entities.isEmpty) { if (!_entities.hasDefaultView) { result.add( Tab( icon: Icon( MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"), size: 24.0, ) ) ); } _entities.viewList.forEach((viewId) { result.add( Tab( icon: MaterialDesignIcons.createIconWidgetFromEntityData(_entities.get(viewId), 24.0, null) ?? Icon( MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"), size: 24.0, ) ) ); }); } return result; } Widget _buildAppTitle() { Row titleRow = Row( children: [Text(_homeAssistant != null ? _homeAssistant.locationName : "")], ); if (_isLoading) { titleRow.children.add(Padding( child: JumpingDotsProgressIndicator( fontSize: 26.0, color: Colors.white, ), padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 30.0), )); } return titleRow; } Drawer _buildAppDrawer() { return new Drawer( child: ListView( children: [ new UserAccountsDrawerHeader( accountName: Text(_homeAssistant != null ? _homeAssistant.locationName : "Unknown"), accountEmail: Text(_instanceHost ?? "Not configured"), currentAccountPicture: new Image.asset('images/hassio-192x192.png'), ), new ListTile( leading: Icon(Icons.settings), title: Text("Connection settings"), onTap: () { Navigator.of(context).pop(); Navigator.of(context).pushNamed('/connection-settings'); }, ), Container( height: 16.0, decoration: new BoxDecoration( border: new Border( bottom: BorderSide( width: 1.0, color: Colors.black26, ) ), ) ), new ListTile( leading: Icon(Icons.insert_drive_file), title: Text("Log"), onTap: () { Navigator.of(context).pop(); Navigator.of(context).pushNamed('/log-view'); }, ), new ListTile( leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")), title: Text("Report an issue"), onTap: () { Navigator.of(context).pop(); HAUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new"); }, ), Container( height: 16.0, decoration: new BoxDecoration( border: new Border( bottom: BorderSide( width: 1.0, color: Colors.black26, ) ), ) ), new AboutListTile( applicationName: appName, applicationVersion: appVersion, applicationLegalese: "Keyboard Crumbs | www.keyboardcrumbs.io", ), new ListTile( leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:coffee")), title: Text("Buy me a coffee"), onTap: () { Navigator.of(context).pop(); HAUtils.launchURL("https://www.buymeacoffee.com/estevez"); }, ) ], ), ); } _checkShowInfo(BuildContext context) { if (_errorCodeToBeShown > 0) { String message = _lastErrorMessage; SnackBarAction action; switch (_errorCodeToBeShown) { case 9: case 11: case 7: case 1: { action = SnackBarAction( label: "Retry", onPressed: () { _scaffoldKey?.currentState?.hideCurrentSnackBar(); _refreshData(); }, ); break; } case 5: { message = "Check connection settings"; action = SnackBarAction( label: "Open", onPressed: () { _scaffoldKey?.currentState?.hideCurrentSnackBar(); Navigator.pushNamed(context, '/connection-settings'); }, ); break; } case 6: { action = SnackBarAction( label: "Settings", onPressed: () { _scaffoldKey?.currentState?.hideCurrentSnackBar(); Navigator.pushNamed(context, '/connection-settings'); }, ); break; } case 10: { action = SnackBarAction( label: "Refresh", onPressed: () { _scaffoldKey?.currentState?.hideCurrentSnackBar(); _refreshData(); }, ); break; } case 8: { action = SnackBarAction( label: "Reconnect", onPressed: () { _scaffoldKey?.currentState?.hideCurrentSnackBar(); _refreshData(); }, ); break; } } Timer(Duration(seconds: 1), () { _scaffoldKey.currentState.hideCurrentSnackBar(); _scaffoldKey.currentState.showSnackBar( SnackBar( content: Text("$message (code: $_errorCodeToBeShown)"), action: action, duration: Duration(hours: 1), ) ); }); } else { _scaffoldKey?.currentState?.hideCurrentSnackBar(); } } final GlobalKey _scaffoldKey = new GlobalKey(); Scaffold _buildScaffold(bool empty) { return Scaffold( key: _scaffoldKey, appBar: AppBar( title: _buildAppTitle(), bottom: empty ? null : TabBar(tabs: buildUIViewTabs()), ), drawer: _buildAppDrawer(), body: empty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"), size: 100.0, color: _errorCodeToBeShown == 0 ? Colors.blue : Colors.redAccent, ), ] ), ) : _homeAssistant.buildViews(context) ); } @override Widget build(BuildContext context) { _checkShowInfo(context); // This method is rerun every time setState is called. if (_entities == null) { return _buildScaffold(true); } else { return DefaultTabController( length: _uiViewsCount, child: _buildScaffold(false) ); } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); if (_stateSubscription != null) _stateSubscription.cancel(); if (_settingsSubscription != null) _settingsSubscription.cancel(); if (_serviceCallSubscription != null) _serviceCallSubscription.cancel(); if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel(); if (_refreshDataSubscription != null) _refreshDataSubscription.cancel(); _homeAssistant.disconnect(); super.dispose(); } }