Compare commits

...

35 Commits

Author SHA1 Message Date
b89b5dfb98 version 0.2.0 2018-09-29 18:13:00 +03:00
a196b0d8d4 Close entity view after setting input value 2018-09-29 18:09:17 +03:00
95f7c14296 Fix entity name padding 2018-09-29 18:02:29 +03:00
2fcd27d240 Fix state handling on entity view page 2018-09-29 17:59:38 +03:00
6834f2ca34 Resolves #52, Resolves #54 Inputs 2018-09-29 17:38:00 +03:00
c0a9b89d40 Resolves #26 Entity view page 2018-09-29 16:19:01 +03:00
067ccfde02 Refactoring: Entity classes by action type. Wntity widget building in
entity
2018-09-29 13:49:25 +03:00
4b4fc338f6 Resolves #91: input_boolean action support 2018-09-29 12:15:31 +03:00
08c07e8398 Resolves #92 Spelling fix 2018-09-29 12:09:01 +03:00
df04d000b2 Fixes #94 Gropups state change event parsing 2018-09-29 12:02:41 +03:00
d0d1ab2740 New card ui. Input_number mode: slider support 2018-09-29 11:52:17 +03:00
af3a5bc611 Resolves #70 Build default_view automatically 2018-09-28 13:33:15 +03:00
b935a0e372 Optimize View creation 2018-09-28 11:23:48 +03:00
49444ab3df Refactoring: badges and cards 2018-09-28 11:18:37 +03:00
098a556279 Massive refactoring: UIBuilder, Vew, HACArd, Badge 2018-09-28 10:16:15 +03:00
375ae36884 Massive refactoring: HomeAssistant, EntityCollection, Entity 2018-09-27 14:51:57 +03:00
0b42019ef3 [WIP] Entity collection 2018-09-26 22:16:50 +03:00
516d38a8a9 version code fix 2018-09-26 00:00:47 +03:00
fb886a4622 Version 0.1.3 2018-09-25 23:41:14 +03:00
662b44d443 CHeck if entity for card has attributes 2018-09-25 23:14:33 +03:00
f9c48e6cc7 Resolves #84: default icon for media players 2018-09-25 22:50:52 +03:00
88d6e1008f Resolves #82: default icon for scenes 2018-09-25 22:48:39 +03:00
4540fadf1e Code refactoring 2018-09-25 22:47:06 +03:00
bd13d3693d Remove state change event messages from log 2018-09-25 22:23:28 +03:00
5db9d6005f Resolves #86 #89: Add 'copy to lipboard' and 'post to github' to log 2018-09-25 22:11:31 +03:00
7e4f744598 Closes #88: Remove version suffix 2018-09-25 21:19:11 +03:00
772b569da5 Minor code cleaning 2018-09-24 22:54:51 +03:00
0e11c1a146 Closes #60 Hide app drawer on item tap 2018-09-24 22:23:01 +03:00
60793dbf89 Add link to github in app drawer 2018-09-24 22:12:56 +03:00
2b622cff04 version 0.1.2 2018-09-24 20:32:16 +03:00
94bcc30421 Fix #77 Skip non existing entities in view 2018-09-24 20:28:17 +03:00
94f43ded6f Fix: #75 Hadle any exception and show it in log view 2018-09-24 20:07:22 +03:00
7f7be8aa78 [#74] Remove floating button 2018-09-24 10:42:31 +03:00
c0e0059487 Settings texts update 2018-09-24 10:27:08 +03:00
23d3d1839f Merge pull request #71 from estevez-dev/release/0.1.1-alpha
Release/0.1.1 alpha
2018-09-24 00:24:13 +03:00
17 changed files with 1206 additions and 614 deletions

View File

@ -39,8 +39,8 @@ android {
applicationId "com.keyboardcrumbs.haclient"
minSdkVersion 21
targetSdkVersion 27
versionCode 19
versionName "0.1.1-alpha"
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

9
lib/badge_class.dart Normal file
View File

@ -0,0 +1,9 @@
part of 'main.dart';
class Badge {
String _entityId;
Badge(String groupId) {
_entityId = groupId;
}
}

25
lib/card_class.dart Normal file
View File

@ -0,0 +1,25 @@
part of 'main.dart';
class HACard {
String _entityId;
List _entities;
String _friendlyName;
List get entities => _entities;
String get friendlyName => _friendlyName;
HACard(String groupId, String friendlyName) {
_entityId = groupId;
_entities = [];
_friendlyName = friendlyName;
}
void addEntity(String entityId) {
_entities.add(entityId);
}
void addEntities(List entities) {
_entities.addAll(entities);
}
}

314
lib/entity.class.dart Normal file
View File

@ -0,0 +1,314 @@
part of 'main.dart';
class Entity {
static const STATE_ICONS_COLORS = {
"on": Colors.amber,
"off": Color.fromRGBO(68, 115, 158, 1.0),
"unavailable": Colors.black12,
"unknown": Colors.black12,
"playing": Colors.amber
};
static const RIGTH_WIDGET_PADDING = 14.0;
static const LEFT_WIDGET_PADDING = 8.0;
static const EXTENDED_WIDGET_HEIGHT = 50.0;
static const WIDGET_HEIGHT = 34.0;
Map _attributes;
String _domain;
String _entityId;
String _state;
String _entityPicture;
DateTime _lastUpdated;
String get displayName => _attributes["friendly_name"] ?? (_attributes["name"] ?? "_");
String get domain => _domain;
String get entityId => _entityId;
String get state => _state;
set state(value) => _state = value;
double get minValue => _attributes["min"] ?? 0.0;
double get maxValue => _attributes["max"] ?? 100.0;
double get valueStep => _attributes["step"] ?? 1.0;
double get doubleState => double.tryParse(_state) ?? 0.0;
bool get isSliderField => _attributes["mode"] == "slider";
bool get isTextField => _attributes["mode"] == "text";
bool get isPasswordField => _attributes["mode"] == "password";
String get deviceClass => _attributes["device_class"] ?? null;
bool get isView => (_domain == "group") && (_attributes != null ? _attributes["view"] ?? false : false);
bool get isGroup => _domain == "group";
String get icon => _attributes["icon"] ?? "";
bool get isOn => state == "on";
String get entityPicture => _attributes["entity_picture"];
String get unitOfMeasurement => _attributes["unit_of_measurement"] ?? "";
List get childEntities => _attributes["entity_id"] ?? [];
String get lastUpdated => _getLastUpdatedFormatted();
Entity(Map rawData) {
update(rawData);
}
int getValueDivisions() {
return ((maxValue - minValue)/valueStep).round().round();
}
void update(Map rawData) {
_attributes = rawData["attributes"] ?? {};
_domain = rawData["entity_id"].split(".")[0];
_entityId = rawData["entity_id"];
_state = rawData["state"];
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
}
String _getLastUpdatedFormatted() {
if (_lastUpdated == null) {
return "-";
} else {
return formatDate(_lastUpdated, [yy, '-', M, '-', d, ' ', HH, ':', nn, ':', ss]);
}
}
void openEntityPage() {
eventBus.fire(new ShowEntityPageEvent(this));
}
Widget buildWidget(BuildContext context) {
return SizedBox(
height: Entity.WIDGET_HEIGHT,
child: Row(
children: <Widget>[
GestureDetector(
child: _buildIconWidget(),
onTap: openEntityPage,
),
Expanded(
child: GestureDetector(
child: _buildNameWidget(),
onTap: openEntityPage,
),
),
_buildActionWidget(context)
],
),
);
}
Widget buildExtendedWidget(BuildContext context, String staticState) {
return Row(
children: <Widget>[
_buildIconWidget(),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: _buildNameWidget(),
),
_buildExtendedActionWidget(context, staticState)
],
),
_buildLastUpdatedWidget()
],
),
)
],
);
}
Widget _buildIconWidget() {
return Padding(
padding: EdgeInsets.fromLTRB(Entity.LEFT_WIDGET_PADDING, 0.0, 12.0, 0.0),
child: MaterialDesignIcons.createIconWidgetFromEntityData(this, 28.0, Entity.STATE_ICONS_COLORS[_state] ?? Colors.blueGrey),
);
}
Widget _buildLastUpdatedWidget() {
return Text(
'${this.lastUpdated}',
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 12.0,
color: Colors.black26
),
);
}
Widget _buildNameWidget() {
return Padding(
padding: EdgeInsets.only(right: 10.0),
child: Text(
"${this.displayName}",
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle(
fontSize: 16.0
),
),
);
}
Widget _buildActionWidget(BuildContext context) {
return Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, Entity.RIGTH_WIDGET_PADDING, 0.0),
child: GestureDetector(
child: Text(
this.isPasswordField ? "******" :
"$_state${this.unitOfMeasurement}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: 16.0,
)
),
onTap: openEntityPage,
)
);
}
Widget _buildExtendedActionWidget(BuildContext context, String staticState) {
return _buildActionWidget(context);
}
}
class SwitchEntity extends Entity {
SwitchEntity(Map rawData) : super(rawData);
@override
Widget _buildActionWidget(BuildContext context) {
return Switch(
value: this.isOn,
onChanged: ((switchState) {
eventBus.fire(new ServiceCallEvent(_domain, switchState ? "turn_on" : "turn_off", entityId, null));
}),
);
}
}
class ButtonEntity extends Entity {
ButtonEntity(Map rawData) : super(rawData);
@override
Widget _buildActionWidget(BuildContext context) {
return FlatButton(
onPressed: (() {
eventBus.fire(new ServiceCallEvent(_domain, "turn_on", _entityId, null));
}),
child: Text(
"EXECUTE",
textAlign: TextAlign.right,
style: new TextStyle(fontSize: 16.0, color: Colors.blue),
),
);
}
}
class InputEntity extends Entity {
InputEntity(Map rawData) : super(rawData);
@override
Widget buildExtendedWidget(BuildContext context, String staticState) {
return Column(
children: <Widget>[
SizedBox(
height: Entity.EXTENDED_WIDGET_HEIGHT,
child: Row(
children: <Widget>[
_buildIconWidget(),
Expanded(
child: _buildNameWidget(),
),
_buildLastUpdatedWidget()
],
),
),
SizedBox(
height: Entity.EXTENDED_WIDGET_HEIGHT,
child: _buildExtendedActionWidget(context, staticState),
)
],
);
}
@override
Widget _buildActionWidget(BuildContext context) {
if (this.isSliderField) {
return Container(
width: 200.0,
child: Row(
children: <Widget>[
Expanded(
child: Slider(
min: this.minValue*10,
max: this.maxValue*10,
value: (this.doubleState <= this.maxValue) && (this.doubleState >= this.minValue) ? this.doubleState*10 : this.minValue*10,
divisions: this.getValueDivisions(),
onChanged: (value) {
eventBus.fire(new StateChangedEvent(_entityId, (value.roundToDouble() / 10).toString(), true));
},
onChangeEnd: (value) {
eventBus.fire(new ServiceCallEvent(_domain, "set_value", _entityId,{"value": "$_state"}));
},
),
),
Padding(
padding: EdgeInsets.only(right: 16.0),
child: Text(
"$_state${this.unitOfMeasurement}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: 16.0,
)
),
)
],
),
);
} else {
return super._buildActionWidget(context);
}
}
@override
Widget _buildExtendedActionWidget(BuildContext context, String staticState) {
return Padding(
padding: EdgeInsets.fromLTRB(Entity.LEFT_WIDGET_PADDING, 0.0, Entity.RIGTH_WIDGET_PADDING, 0.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: TextField(
obscureText: this.isPasswordField,
controller: TextEditingController(
text: staticState,
),
onChanged: (value) {
staticState = value;
},
),
),
SizedBox(
width: 63.0,
child: FlatButton(
onPressed: () {
eventBus.fire(new ServiceCallEvent(_domain, "set_value", _entityId,{"value": "$staticState"}));
Navigator.pop(context);
},
child: Text(
"SET",
textAlign: TextAlign.right,
style: new TextStyle(fontSize: 16.0, color: Colors.blue),
),
),
)
],
)
);
}
}

65
lib/entity.page.dart Normal file
View File

@ -0,0 +1,65 @@
part of 'main.dart';
class EntityViewPage extends StatefulWidget {
EntityViewPage({Key key, this.entity}) : super(key: key);
Entity entity;
@override
_EntityViewPageState createState() => new _EntityViewPageState();
}
class _EntityViewPageState extends State<EntityViewPage> {
String _title;
Entity _entity;
String _lastState;
StreamSubscription _stateSubscription;
@override
void initState() {
super.initState();
_entity = widget.entity;
_lastState = _entity.state;
if (_stateSubscription != null) _stateSubscription.cancel();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
setState(() {
if (event.entityId == _entity.entityId) {
_lastState = event.newState ?? _entity.state;
}
});
});
_prepareData();
}
_prepareData() async {
_title = _entity.displayName;
}
@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(_title),
),
body: Padding(
padding: EdgeInsets.all(10.0),
child: ListView(
children: <Widget>[
_entity.buildExtendedWidget(context, _lastState)
],
),
),
);
}
@override
void dispose(){
if (_stateSubscription != null) _stateSubscription.cancel();
super.dispose();
}
}

View File

@ -0,0 +1,112 @@
part of 'main.dart';
class EntityCollection {
Map<String, Entity> _entities;
List<String> viewList;
EntityCollection() {
_entities = {};
viewList = [];
}
bool get hasDefaultView => _entities["group.default_view"] != null;
void parse(List rawData) {
_entities.clear();
viewList.clear();
TheLogger.log("Debug","Parsing ${rawData.length} Home Assistant entities");
rawData.forEach((rawEntityData) {
Entity newEntity = addFromRaw(rawEntityData);
if (newEntity.isView) {
viewList.add(newEntity.entityId);
}
});
}
Entity _createEntityInstance(rawEntityData) {
switch (rawEntityData["entity_id"].split(".")[0]) {
case "automation":
case "input_boolean ":
case "switch":
case "light": {
return SwitchEntity(rawEntityData);
}
case "script":
case "scene": {
return ButtonEntity(rawEntityData);
}
case "input_text":
case "input_number": {
return InputEntity(rawEntityData);
}
default: {
return Entity(rawEntityData);
}
}
}
void updateState(Map rawStateData) {
if (isExist(rawStateData["entity_id"])) {
updateFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
} else {
addFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
}
}
void add(Entity entity) {
_entities[entity.entityId] = entity;
}
Entity addFromRaw(Map rawEntityData) {
Entity entity = _createEntityInstance(rawEntityData);
_entities[entity.entityId] = entity;
return entity;
}
void updateFromRaw(Map rawEntityData) {
//TODO pass entity in this function and call update from it
_entities[rawEntityData["entity_id"]].update(rawEntityData);
}
Entity get(String entityId) {
return _entities[entityId];
}
bool isExist(String entityId) {
return _entities[entityId] != null;
}
Map<String,List<String>> getDefaultViewTopLevelEntities() {
Map<String,List<String>> result = {"userGroups": [], "notGroupedEntities": []};
List<String> entities = [];
_entities.forEach((id, entity){
if ((id.indexOf("group.") == 0) && (id.indexOf(".all_") == -1) && (!entity.isView)) {
result["userGroups"].add(id);
}
if (!entity.isGroup) {
entities.add(id);
}
});
entities.forEach((entiyId) {
bool foundInGroup = false;
result["userGroups"].forEach((userGroupId) {
if (_entities[userGroupId].childEntities.contains(entiyId)) {
foundInGroup = true;
}
});
if (!foundInGroup) {
result["notGroupedEntities"].add(entiyId);
}
});
return result;
}
}

View File

@ -0,0 +1,260 @@
part of 'main.dart';
class HomeAssistant {
String _hassioAPIEndpoint;
String _hassioPassword;
String _hassioAuthType;
IOWebSocketChannel _hassioChannel;
int _currentMessageId = 0;
int _statesMessageId = 0;
int _servicesMessageId = 0;
int _subscriptionMessageId = 0;
int _configMessageId = 0;
EntityCollection _entities;
UIBuilder _uiBuilder;
Map _instanceConfig = {};
Completer _fetchCompleter;
Completer _statesCompleter;
Completer _servicesCompleter;
Completer _configCompleter;
Timer _fetchingTimer;
String get locationName => _instanceConfig["location_name"] ?? "";
int get viewsCount => _entities.viewList.length ?? 0;
UIBuilder get uiBuilder => _uiBuilder;
EntityCollection get entities => _entities;
HomeAssistant(String url, String password, String authType) {
_hassioAPIEndpoint = url;
_hassioPassword = password;
_hassioAuthType = authType;
_entities = EntityCollection();
_uiBuilder = UIBuilder();
}
Future fetch() {
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
TheLogger.log("Warning","Previous fetch is not complited");
} else {
//TODO: Fetch timeout timer. Should be removed after #21 fix
_fetchingTimer = Timer(Duration(seconds: 15), () {
closeConnection();
_fetchCompleter.completeError({"errorCode" : 1,"errorMessage": "Connection timeout"});
});
_fetchCompleter = new Completer();
_reConnectSocket().then((r) {
_getData();
}).catchError((e) {
_finishFetching(e);
});
}
return _fetchCompleter.future;
}
closeConnection() {
if (_hassioChannel?.closeCode == null) {
_hassioChannel?.sink?.close();
}
_hassioChannel = null;
}
Future _reConnectSocket() {
var _connectionCompleter = new Completer();
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
TheLogger.log("Debug","Socket connecting...");
_hassioChannel = IOWebSocketChannel.connect(_hassioAPIEndpoint);
_hassioChannel.stream.handleError((e) {
TheLogger.log("Error","Unhandled socket error: ${e.toString()}");
});
_hassioChannel.stream.listen((message) =>
_handleMessage(_connectionCompleter, message));
} else {
_connectionCompleter.complete();
}
return _connectionCompleter.future;
}
_getData() {
_getConfig().then((result) {
_getStates().then((result) {
_getServices().then((result) {
_finishFetching(null);
}).catchError((e) {
_finishFetching(e);
});
}).catchError((e) {
_finishFetching(e);
});
}).catchError((e) {
_finishFetching(e);
});
}
_finishFetching(error) {
_fetchingTimer.cancel();
if (error != null) {
_fetchCompleter.completeError(error);
} else {
_fetchCompleter.complete();
}
}
_handleMessage(Completer connectionCompleter, String message) {
var data = json.decode(message);
//TheLogger.log("Debug","[Received] => Message type: ${data['type']}");
if (data["type"] == "auth_required") {
_sendMessageRaw('{"type": "auth","$_hassioAuthType": "$_hassioPassword"}');
} else if (data["type"] == "auth_ok") {
_sendSubscribe();
connectionCompleter.complete();
} else if (data["type"] == "auth_invalid") {
connectionCompleter.completeError({"errorCode": 6, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") {
if (data["id"] == _configMessageId) {
_parseConfig(data);
} else if (data["id"] == _statesMessageId) {
_parseEntities(data);
} else if (data["id"] == _servicesMessageId) {
_parseServices(data);
} else if (data["id"] == _currentMessageId) {
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) {
TheLogger.log("Warning","Unhandled event type: ${data["event"]["event_type"]}");
} else {
TheLogger.log("Error","Event is null: $message");
}
} else {
TheLogger.log("Warning","Unknown message type: $message");
}
}
void _sendSubscribe() {
_incrementMessageId();
_subscriptionMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}');
}
Future _getConfig() {
_configCompleter = new Completer();
_incrementMessageId();
_configMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}');
return _configCompleter.future;
}
Future _getStates() {
_statesCompleter = new Completer();
_incrementMessageId();
_statesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}');
return _statesCompleter.future;
}
Future _getServices() {
_servicesCompleter = new Completer();
_incrementMessageId();
_servicesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}');
return _servicesCompleter.future;
}
_incrementMessageId() {
_currentMessageId += 1;
}
_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) {
TheLogger.log("Debug", "Parsing new state for ${eventData['entity_id']}");
_entities.updateState(eventData);
eventBus.fire(new StateChangedEvent(eventData["entity_id"], null, false));
}
void _parseConfig(Map data) {
if (data["success"] == true) {
_instanceConfig = Map.from(data["result"]);
_configCompleter.complete();
} else {
_configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]});
}
}
void _parseServices(response) {
_servicesCompleter.complete();
/*if (response["success"] == false) {
_servicesCompleter.completeError({"errorCode": 4, "errorMessage": response["error"]["message"]});
return;
}
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.isExist("$domain.$serviceName")) {
result[domain].remove(serviceName);
}
});
});
_servicesData = result;
_servicesCompleter.complete();
} catch (e) {
TheLogger.log("Error","Error parsing services. But they are not used :-)");
_servicesCompleter.complete();
}*/
}
void _parseEntities(response) async {
if (response["success"] == false) {
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
return;
}
_entities.parse(response["result"]);
_uiBuilder.build(_entities);
_statesCompleter.complete();
}
Future callService(String domain, String service, String entityId, Map<String, String> additionalParams) {
var sendCompleter = Completer();
//TODO: Send service call timeout timer. Should be removed after #21 fix
Timer _sendTimer = Timer(Duration(seconds: 7), () {
sendCompleter.completeError({"errorCode" : 8,"errorMessage": "Connection timeout"});
});
_reConnectSocket().then((r) {
_incrementMessageId();
String message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
if (additionalParams != null) {
additionalParams.forEach((name, value){
message += ', "$name" : "$value"';
});
}
message += '}}';
_sendMessageRaw(message);
_sendTimer.cancel();
sendCompleter.complete();
}).catchError((e){
_sendTimer.cancel();
sendCompleter.completeError(e);
});
return sendCompleter.future;
}
}

View File

@ -10,11 +10,7 @@ class LogViewPage extends StatefulWidget {
}
class _LogViewPageState extends State<LogViewPage> {
String _hassioDomain = "";
String _hassioPort = "8123";
String _hassioPassword = "";
String _socketProtocol = "wss";
String _authType = "access_token";
String _logData;
@override
void initState() {
@ -23,7 +19,7 @@ class _LogViewPageState extends State<LogViewPage> {
}
_loadLog() async {
//
_logData = TheLogger.getLog();
}
@override
@ -36,12 +32,28 @@ class _LogViewPageState extends State<LogViewPage> {
// 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),
actions: <Widget>[
IconButton(
icon: Icon(Icons.content_copy),
onPressed: () {
Clipboard.setData(new ClipboardData(text: _logData));
},
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")),
onPressed: () {
String body = "```\n$_logData```";
String encodedBody = "${Uri.encodeFull(body)}";
haUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new?body=$encodedBody");
},
),
],
),
body: TextField(
maxLines: null,
controller: TextEditingController(
text: TheLogger.getLog()
text: _logData
),
)
);

View File

@ -8,50 +8,48 @@ 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 'settingsPage.dart';
part 'data_model.dart';
part 'logPage.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.class.dart';
part 'entity_collection.class.dart';
part 'ui_builder_class.dart';
part 'view_class.dart';
part 'card_class.dart';
part 'badge_class.dart';
EventBus eventBus = new EventBus();
const String appName = "HA Client";
const appVersion = "0.1.1-alpha";
const appVersion = "0.2.0";
String homeAssistantWebHost;
class TheLogger {
static List<String> _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() {
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");
}
});
}
void main() => runApp(new HassClientApp());
class HassClientApp extends StatelessWidget {
class HAClientApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
@ -80,24 +78,19 @@ class MainPage extends StatefulWidget {
}
class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
HassioDataModel _dataModel;
Map _entitiesData;
Map _uiStructure;
Map _instanceConfig;
HomeAssistant _homeAssistant;
EntityCollection _entities;
//Map _instanceConfig;
int _uiViewsCount = 0;
String _instanceHost;
int _errorCodeToBeShown = 0;
String _lastErrorMessage = "";
StreamSubscription _stateSubscription;
StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription;
bool _isLoading = true;
Map<String, Color> _stateIconColors = {
"on": Colors.amber,
"off": Color.fromRGBO(68, 115, 158, 1.0),
"unavailable": Colors.black12,
"unknown": Colors.black12,
"playing": Colors.amber
};
Map<String, Color> _badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
@ -140,20 +133,33 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_errorCodeToBeShown = 5;
});
} else {
if (_dataModel != null) _dataModel.closeConnection();
if (_homeAssistant != null) _homeAssistant.closeConnection();
_createConnection(apiEndpoint, apiPassword, authType);
}
}
_createConnection(String apiEndpoint, String apiPassword, String authType) {
_dataModel = HassioDataModel(apiEndpoint, apiPassword, authType);
_homeAssistant = HomeAssistant(apiEndpoint, apiPassword, authType);
_refreshData();
if (_stateSubscription != null) _stateSubscription.cancel();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
setState(() {
_entitiesData = _dataModel.entities;
if (event.localChange) {
_entities
.get(event.entityId)
.state = event.newState;
}
});
});
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
_serviceCallSubscription = eventBus.on<ServiceCallEvent>().listen((event) {
_callService(event.domain, event.service, event.entityId, event.additionalParams);
});
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
_showEntityPageSubscription = eventBus.on<ShowEntityPageEvent>().listen((event) {
_showEntityPage(event.entity);
});
}
_refreshData() async {
@ -161,13 +167,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_isLoading = true;
});
_errorCodeToBeShown = 0;
if (_dataModel != null) {
await _dataModel.fetch().then((result) {
if (_homeAssistant != null) {
await _homeAssistant.fetch().then((result) {
setState(() {
_instanceConfig = _dataModel.instanceConfig;
_entitiesData = _dataModel.entities;
_uiStructure = _dataModel.uiStructure;
_uiViewsCount = _uiStructure.length;
//_instanceConfig = _homeAssistant.instanceConfig;
_entities = _homeAssistant.entities;
_uiViewsCount = _homeAssistant.viewsCount;
_isLoading = false;
});
}).catchError((e) {
@ -184,27 +189,36 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
});
}
void _callService(String domain, String service, String entityId) {
void _callService(String domain, String service, String entityId, Map<String, String> additionalParams) {
setState(() {
_isLoading = true;
});
_dataModel.callService(domain, service, entityId).then((r) {
_homeAssistant.callService(domain, service, entityId, additionalParams).then((r) {
setState(() {
_isLoading = false;
});
}).catchError((e) => _setErrorState(e));
}
void _showEntityPage(Entity entity) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EntityViewPage(entity: entity),
)
);
}
List<Widget> _buildViews() {
List<Widget> result = [];
if ((_entitiesData != null) && (_uiStructure != null)) {
_uiStructure.forEach((viewId, structure) {
if ((_entities != null) && (!_homeAssistant.uiBuilder.isEmpty)) {
_homeAssistant.uiBuilder.views.forEach((viewId, view) {
result.add(
RefreshIndicator(
color: Colors.amber,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: _buildSingleView(structure),
children: _buildSingleView(view),
),
onRefresh: () => _refreshData(),
)
@ -214,52 +228,49 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
return result;
}
List<Widget> _buildSingleView(structure) {
List<Widget> _buildSingleView(View view) {
List<Widget> result = [];
if (structure["badges"]["children"].length > 0) {
if (view.isThereBadges) {
result.add(
Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 4.0,
//padding: new EdgeInsets.all(8.0),
//itemExtent: 40.0,
children: _buildBadges(structure["badges"]["children"]),
)
Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 1.0,
children: _buildBadges(view.badges),
)
);
}
structure["groups"].forEach((id, group) {
if (group["children"].length > 0) {
result.add(_buildCard(
group["children"], group["friendly_name"].toString()));
view.cards.forEach((id, card) {
if (card.entities.isNotEmpty) {
result.add(_buildCard(card));
}
});
return result;
}
List<Widget> _buildBadges(List ids) {
List<Widget> _buildBadges( Map<String, Badge> badges) {
List<Widget> result = [];
ids.forEach((entityId) {
var data = _entitiesData[entityId];
if (data != null) {
badges.forEach((id, badge) {
var badgeEntity = _entities.get(id);
if (badgeEntity != null) {
result.add(
_buildSingleBadge(data)
_buildSingleBadge(badgeEntity)
);
}
});
return result;
}
Widget _buildSingleBadge(data) {
Widget _buildSingleBadge(Entity data) {
double iconSize = 26.0;
Widget badgeIcon;
String badgeTextValue;
Color iconColor = _badgeColors[data["domain"]] ?? _badgeColors["default"];
switch (data["domain"]) {
Color iconColor = _badgeColors[data.domain] ?? _badgeColors["default"];
switch (data.domain) {
case "sun": {
badgeIcon = data["state"] == "below_horizon" ?
badgeIcon = data.state == "below_horizon" ?
Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
size: iconSize,
@ -271,35 +282,35 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
break;
}
case "sensor": {
badgeTextValue = data["attributes"]["unit_of_measurement"];
badgeTextValue = data.unitOfMeasurement;
badgeIcon = Center(
child: Text(
"${data['state']}",
"${data.state == 'unknown' ? '-' : data.state}",
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18.0),
style: TextStyle(fontSize: 17.0),
),
);
break;
}
case "device_tracker": {
badgeIcon = MaterialDesignIcons.createIconFromEntityData(data, iconSize,Colors.black);
badgeTextValue = data["state"];
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(data, iconSize,Colors.black);
badgeTextValue = data.state;
break;
}
default: {
badgeIcon = MaterialDesignIcons.createIconFromEntityData(data, iconSize,Colors.black);
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(data, iconSize,Colors.black);
}
}
Widget badgeText;
if (badgeTextValue == null) {
if (badgeTextValue == null || badgeTextValue.length == 0) {
badgeText = Container(width: 0.0, height: 0.0);
} else {
badgeText = Container(
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
child: Text("$badgeTextValue",
style: TextStyle(fontSize: 13.0, color: Colors.white),
style: TextStyle(fontSize: 12.0, color: Colors.white),
textAlign: TextAlign.center, softWrap: false, overflow: TextOverflow.fade),
decoration: new BoxDecoration(
// Circle shape
@ -338,8 +349,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
Positioned(
//width: 50.0,
bottom: -9.0,
left: -15.0,
right: -15.0,
left: -10.0,
right: -10.0,
child: Center(
child: badgeText,
)
@ -350,10 +361,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
Container(
width: 60.0,
child: Text(
"${data['display_name']}",
"${data.displayName}",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12.0),
softWrap: true,
maxLines: 2,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
@ -361,12 +373,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
);
}
Card _buildCard(List ids, String name) {
Card _buildCard(HACard card) {
List<Widget> body = [];
body.add(_buildCardHeader(name));
body.addAll(_buildCardBody(ids));
Card result =
Card(child: new Column(mainAxisSize: MainAxisSize.min, children: body));
body.add(_buildCardHeader(card.friendlyName));
body.addAll(_buildCardBody(card.entities));
Card result = Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
);
return result;
}
@ -391,90 +404,29 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
List<Widget> _buildCardBody(List ids) {
List<Widget> entities = [];
ids.forEach((id) {
var data = _entitiesData[id];
if (data != null) {
entities.add(new ListTile(
leading: MaterialDesignIcons.createIconFromEntityData(data, 28.0, _stateIconColors[data["state"]] ?? Colors.blueGrey),
//subtitle: Text("${data['entity_id']}"),
trailing: _buildEntityActionWidget(data),
title: Text(
"${data["display_name"]}",
overflow: TextOverflow.fade,
softWrap: false,
),
));
var entity = _entities.get(id);
if (entity != null) {
entities.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
child: entity.buildWidget(context),
));
}
});
return entities;
}
Widget _buildEntityActionWidget(data) {
String entityId = data["entity_id"];
Widget result;
switch (data["domain"]) {
case "automation":
case "switch":
case "light": {
result = Switch(
value: (data["state"] == "on"),
onChanged: ((state) {
_callService(
data["domain"], state ? "turn_on" : "turn_off", entityId);
setState(() {
_entitiesData[entityId]["state"] = state ? "on" : "off";
});
}),
);
break;
}
case "script":
case "scene": {
result = SizedBox(
width: 60.0,
child: FlatButton(
onPressed: (() {
_callService(data["domain"], "turn_on", entityId);
}),
child: Text(
"Run",
textAlign: TextAlign.right,
style: new TextStyle(fontSize: 16.0, color: Colors.blue),
),
)
);
break;
}
default: {
result = Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 16.0, 0.0),
child: Text(
"${data["state"]}${(data["attributes"] != null && data["attributes"]["unit_of_measurement"] != null) ? data["attributes"]["unit_of_measurement"] : ''}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: 16.0,
)
)
);
}
}
/*return SizedBox(
width: 60.0,
// height: double.infinity,
child: result
);*/
return result;
}
List<Tab> buildUIViewTabs() {
List<Tab> result = [];
if ((_entitiesData != null) && (_uiStructure != null)) {
_uiStructure.forEach((viewId, structure) {
if ((_entities != null) && (!_homeAssistant.uiBuilder.isEmpty)) {
_homeAssistant.uiBuilder.views.forEach((viewId, view) {
result.add(
Tab(
icon: MaterialDesignIcons.createIconFromEntityData(structure, 24.0, null)
icon: MaterialDesignIcons.createIconWidgetFromEntityData(_entities.get(viewId), 24.0, null) ??
Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
size: 24.0,
)
)
);
});
@ -484,7 +436,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
Widget _buildAppTitle() {
Row titleRow = Row(
children: [Text(_instanceConfig != null ? _instanceConfig["location_name"] : "")],
children: [Text(_homeAssistant != null ? _homeAssistant.locationName : "")],
);
if (_isLoading) {
titleRow.children.add(Padding(
@ -503,7 +455,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
child: ListView(
children: <Widget>[
new UserAccountsDrawerHeader(
accountName: Text(_instanceConfig != null ? _instanceConfig["location_name"] : "Unknown"),
accountName: Text(_homeAssistant != null ? _homeAssistant.locationName : "Unknown"),
accountEmail: Text(_instanceHost ?? "Not configured"),
currentAccountPicture: new Image.asset('images/hassio-192x192.png'),
),
@ -511,14 +463,24 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
leading: Icon(Icons.settings),
title: Text("Connection settings"),
onTap: () {
Navigator.pushNamed(context, '/connection-settings');
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/connection-settings');
},
),
new ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text("Log"),
onTap: () {
Navigator.pushNamed(context, '/log-view');
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");
},
),
new AboutListTile(
@ -609,62 +571,44 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
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,
),
]
),
)
:
TabBarView(
children: _buildViews()
),
);
}
@override
Widget build(BuildContext context) {
_checkShowInfo(context);
// This method is rerun every time setState is called.
//
if (_entitiesData == null) {
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
title: _buildAppTitle()
),
drawer: _buildAppDrawer(),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
/*Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 10.0),
child: Text(
_fetchErrorCode > 0 ? "Well... no.\n\nThere was an error [$_fetchErrorCode]: ${_getErrorMessageByCode(_fetchErrorCode, false)}" : "Loading...",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16.0),
),
),*/
Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
size: 100.0,
color: _errorCodeToBeShown == 0 ? Colors.blue : Colors.redAccent,
),
]
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _refreshData,
tooltip: 'Increment',
child: new Icon(Icons.refresh),
),
);
if (_entities == null) {
return _buildScaffold(true);
} else {
return DefaultTabController(
length: _uiViewsCount,
child: new Scaffold(
key: _scaffoldKey,
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: _buildAppTitle(),
bottom: TabBar(
tabs: buildUIViewTabs()
),
),
drawer: _buildAppDrawer(),
body: TabBarView(
children: _buildViews()
),
)
child: _buildScaffold(false)
);
}
}
@ -674,7 +618,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
WidgetsBinding.instance.removeObserver(this);
if (_stateSubscription != null) _stateSubscription.cancel();
if (_settingsSubscription != null) _settingsSubscription.cancel();
_dataModel.closeConnection();
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
_homeAssistant.closeConnection();
super.dispose();
}
}

View File

@ -1,362 +1,5 @@
part of 'main.dart';
class StateChangedEvent {
String entityId;
StateChangedEvent(this.entityId);
}
class SettingsChangedEvent {
bool reconnect;
SettingsChangedEvent(this.reconnect);
}
class HassioDataModel {
String _hassioAPIEndpoint;
String _hassioPassword;
String _hassioAuthType;
IOWebSocketChannel _hassioChannel;
int _currentMessageId = 0;
int _statesMessageId = 0;
int _servicesMessageId = 0;
int _subscriptionMessageId = 0;
int _configMessageId = 0;
Map _entitiesData = {};
Map _servicesData = {};
Map _uiStructure = {};
Map _instanceConfig = {};
Completer _fetchCompleter;
Completer _statesCompleter;
Completer _servicesCompleter;
Completer _configCompleter;
Timer _fetchingTimer;
List _topBadgeDomains = ["alarm_control_panel", "binary_sensor", "device_tracker", "updater", "sun", "timer", "sensor"];
Map get entities => _entitiesData;
Map get services => _servicesData;
Map get uiStructure => _uiStructure;
Map get instanceConfig => _instanceConfig;
HassioDataModel(String url, String password, String authType) {
_hassioAPIEndpoint = url;
_hassioPassword = password;
_hassioAuthType = authType;
}
Future fetch() {
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
TheLogger.log("Warning","Previous fetch is not complited");
} else {
//TODO: Fetch timeout timer. Should be removed after #21 fix
_fetchingTimer = Timer(Duration(seconds: 15), () {
closeConnection();
_fetchCompleter.completeError({"errorCode" : 1,"errorMessage": "Connection timeout"});
});
_fetchCompleter = new Completer();
_reConnectSocket().then((r) {
_getData();
}).catchError((e) {
_finishFetching(e);
});
}
return _fetchCompleter.future;
}
closeConnection() {
if (_hassioChannel?.closeCode == null) {
_hassioChannel?.sink?.close();
}
_hassioChannel = null;
}
Future _reConnectSocket() {
var _connectionCompleter = new Completer();
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
TheLogger.log("Debug","Socket connecting...");
_hassioChannel = IOWebSocketChannel.connect(_hassioAPIEndpoint);
_hassioChannel.stream.handleError((e) {
TheLogger.log("Error","Unhandled socket error: ${e.toString()}");
});
_hassioChannel.stream.listen((message) =>
_handleMessage(_connectionCompleter, message));
} else {
_connectionCompleter.complete();
}
return _connectionCompleter.future;
}
_getData() {
_getConfig().then((result) {
_getStates().then((result) {
_getServices().then((result) {
_finishFetching(null);
}).catchError((e) {
_finishFetching(e);
});
}).catchError((e) {
_finishFetching(e);
});
}).catchError((e) {
_finishFetching(e);
});
}
_finishFetching(error) {
_fetchingTimer.cancel();
if (error != null) {
_fetchCompleter.completeError(error);
} else {
_fetchCompleter.complete();
}
}
_handleMessage(Completer connectionCompleter, String message) {
var data = json.decode(message);
TheLogger.log("Debug","[Received] => Message type: ${data['type']}");
if (data["type"] == "auth_required") {
_sendMessageRaw('{"type": "auth","$_hassioAuthType": "$_hassioPassword"}');
} else if (data["type"] == "auth_ok") {
_sendSubscribe();
connectionCompleter.complete();
} else if (data["type"] == "auth_invalid") {
connectionCompleter.completeError({"errorCode": 6, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") {
if (data["id"] == _configMessageId) {
_parseConfig(data);
} else if (data["id"] == _statesMessageId) {
_parseEntities(data);
} else if (data["id"] == _servicesMessageId) {
_parseServices(data);
} else if (data["id"] == _currentMessageId) {
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) {
TheLogger.log("Warning","Unhandled event type: ${data["event"]["event_type"]}");
} else {
TheLogger.log("Error","Event is null: $message");
}
} else {
TheLogger.log("Warning","Unknown message type: $message");
}
}
void _sendSubscribe() {
_incrementMessageId();
_subscriptionMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}');
}
Future _getConfig() {
_configCompleter = new Completer();
_incrementMessageId();
_configMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}');
return _configCompleter.future;
}
Future _getStates() {
_statesCompleter = new Completer();
_incrementMessageId();
_statesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}');
return _statesCompleter.future;
}
Future _getServices() {
_servicesCompleter = new Completer();
_incrementMessageId();
_servicesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}');
return _servicesCompleter.future;
}
_incrementMessageId() {
_currentMessageId += 1;
}
_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) {
TheLogger.log("Debug", "Parsing new state for ${eventData['entity_id']}");
if (eventData["new_state"] == null) {
TheLogger.log("Error", "No new_state found");
} else {
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"]));
}
}
void _parseConfig(Map data) {
if (data["success"] == true) {
_instanceConfig = Map.from(data["result"]);
_configCompleter.complete();
} else {
_configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]});
}
}
void _parseServices(response) {
if (response["success"] == false) {
_servicesCompleter.completeError({"errorCode": 4, "errorMessage": response["error"]["message"]});
return;
}
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();
} 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"];
TheLogger.log("Debug","Parsing ${data.length} Home Assistant entities");
List<String> uiGroups = [];
data.forEach((entity) {
try {
var composedEntity = _parseEntity(entity);
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']}");
}
});
//Gethering information for UI
TheLogger.log("Debug","Gethering views");
int viewCounter = 0;
uiGroups.forEach((viewId) { //Each view
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);
} 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<String>();
_entitiesData[entityId]["attributes"]["entity_id"].forEach((
groupedEntityId) {
newGroup["children"].add(groupedEntityId);
});
viewGroupStructure["groups"]["$entityId"] = Map.from(newGroup);
}
});
}
_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
Timer _sendTimer = Timer(Duration(seconds: 7), () {
sendCompleter.completeError({"errorCode" : 8,"errorMessage": "Connection timeout"});
});
_reConnectSocket().then((r) {
_incrementMessageId();
_sendMessageRaw('{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entity_id"}}');
_sendTimer.cancel();
sendCompleter.complete();
}).catchError((e){
_sendTimer.cancel();
sendCompleter.completeError(e);
});
return sendCompleter.future;
}
}
class MaterialDesignIcons {
static Map _defaultIconsByDomains = {
"light": "mdi:lightbulb",
@ -371,7 +14,9 @@ class MaterialDesignIcons {
"input_number": "mdi:ray-vertex",
"input_select": "mdi:format-list-bulleted",
"input_text": "mdi:textbox",
"sun": "mdi:white-balance-sunny"
"sun": "mdi:white-balance-sunny",
"scene": "mdi:google-pages",
"media_player": "mdi:cast"
};
static Map _defaultIconsByDeviceClass = {
@ -3223,35 +2868,34 @@ class MaterialDesignIcons {
"mdi:blank": 0xf68c
};
static Widget createIconFromEntityData(Map data, double size, Color color) {
if ((data["attributes"] != null) && (data["attributes"]["entity_picture"] != null)) {
static Widget createIconWidgetFromEntityData(Entity data, double size, Color color) {
if (data == null) {
return null;
}
if (data.entityPicture != null) {
if (homeAssistantWebHost != null) {
return CircleAvatar(
backgroundColor: Colors.white,
backgroundImage: CachedNetworkImageProvider(
"$homeAssistantWebHost${data["attributes"]["entity_picture"]}",
"$homeAssistantWebHost${data.entityPicture}",
),
);
} else {
return Container(width: 0.0, height: 0.0);
}
} else {
String iconName = data["attributes"] != null
? data["attributes"]["icon"]
: null;
String iconName = data.icon;
int iconCode = 0;
if (iconName != null) {
if (iconName.length > 0) {
iconCode = getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(data["entity_id"],
data["attributes"] != null
? data["attributes"]["device_class"]
: null, data["state"]); //
iconCode = getDefaultIconByEntityId(data.entityId,
data.deviceClass, data.state); //
}
return Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
);
}
}

View File

@ -57,8 +57,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
});
eventBus.fire(SettingsChangedEvent(true));
}),
// 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: ListView(
@ -66,7 +64,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
children: <Widget>[
new Row(
children: [
Text("HTTPS"),
Text("Use ssl (HTTPS)"),
Switch(
value: (_socketProtocol == "wss"),
onChanged: (value) {
@ -132,4 +130,4 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
),
);
}
}
}

66
lib/ui_builder_class.dart Normal file
View File

@ -0,0 +1,66 @@
part of 'main.dart';
class UIBuilder {
EntityCollection _entities;
Map<String, View> _views;
static List badgeDomains = ["alarm_control_panel", "binary_sensor", "device_tracker", "updater", "sun", "timer", "sensor"];
bool get isEmpty => _views.length == 0;
Map<String, View> get views => _views ?? {};
UIBuilder() {
_views = {};
}
static bool isBadge(String domain) {
return badgeDomains.contains(domain);
}
void build(EntityCollection entitiesCollection) {
_entities = entitiesCollection;
_views.clear();
if (!_entities.hasDefaultView) {
_createDefaultView();
}
_createViews(entitiesCollection.viewList);
}
void _createDefaultView() {
Map<String, List<String>> userGroupsList = _entities.getDefaultViewTopLevelEntities();
TheLogger.log("RESULT", "${userGroupsList["userGroups"]}");
TheLogger.log("RESULT", "${userGroupsList["notGroupedEntities"]}");
View view = View("group.default_view", 0);
userGroupsList["userGroups"].forEach((groupId){
view.add(_entities.get(groupId));
});
userGroupsList["notGroupedEntities"].forEach((entityId){
view.add(_entities.get(entityId));
});
_views["group.default_view"] = view;
}
void _createViews(List<String> viewsList) {
int counter = 0;
viewsList.forEach((viewId) {
counter += 1;
View view = View(viewId, counter);
try {
Entity viewGroupEntity = _entities.get(viewId);
viewGroupEntity.childEntities.forEach((
entityId) { //Each entity or group in view
if (_entities.isExist(entityId)) {
view.add(_entities.get(entityId));
} else {
TheLogger.log("Warning", "Unknown entity inside view: $entityId");
}
});
} catch (error) {
TheLogger.log("Error","Error parsing view: $viewId");
}
_views[viewId] = view;
});
}
}

72
lib/utils.class.dart Normal file
View File

@ -0,0 +1,72 @@
part of 'main.dart';
class TheLogger {
static List<String> _log = [];
static String getLog() {
String res = '';
_log.forEach((line) {
res += "$line\n";
});
return res;
}
static bool get isInDebugMode {
bool inDebugMode = false;
assert(inDebugMode = true);
return inDebugMode;
}
static void log(String level, String message) {
if (isInDebugMode) {
debugPrint('$message');
}
_log.add("[$level] : $message");
if (_log.length > 50) {
_log.removeAt(0);
}
}
}
class haUtils {
static void launchURL(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
TheLogger.log("Error", "Could not launch $url");
}
}
}
class StateChangedEvent {
String entityId;
String newState;
bool localChange;
StateChangedEvent(this.entityId, this.newState, this.localChange);
}
class SettingsChangedEvent {
bool reconnect;
SettingsChangedEvent(this.reconnect);
}
class ServiceCallEvent {
String domain;
String service;
String entityId;
Map<String, String> additionalParams;
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
}
class ShowEntityPageEvent {
Entity entity;
ShowEntityPageEvent(this.entity);
}

53
lib/view_class.dart Normal file
View File

@ -0,0 +1,53 @@
part of 'main.dart';
class View {
String _entityId;
int _count;
Map<String, HACard> cards;
Map<String, Badge> badges;
bool get isThereBadges => (badges != null) && (badges.isNotEmpty);
View(String groupId, int viewCount) {
_entityId = groupId;
_count = viewCount;
cards = {};
badges = {};
}
void add(Entity entity) {
if (!entity.isGroup) {
_addEntityWithoutGroup(entity);
} else {
_addCardWithEntities(entity);
}
}
void _addBadge(String entityId) {
badges.addAll({entityId: Badge(entityId)});
}
void _addEntityWithoutGroup(Entity entity) {
if (UIBuilder.isBadge(entity.domain)) {
//This is badge
_addBadge(entity.entityId);
} else {
//This is a standalone entity
String groupIdToAdd = "${entity.domain}.${entity.domain}$_count";
if (cards[groupIdToAdd] == null) {
_addCard(groupIdToAdd, entity.domain);
}
cards[groupIdToAdd].addEntity(entity.entityId);
}
}
void _addCard(String entityId, String friendlyName) {
cards.addAll({"$entityId": HACard(entityId, friendlyName)});
}
void _addCardWithEntities(Entity entity) {
cards.addAll({"${entity.entityId}": HACard(entity.entityId, entity.displayName)});
cards[entity.entityId].addEntities(entity.childEntities);
}
}

View File

@ -87,6 +87,13 @@ packages:
url: "https://github.com/MarkOSullivan94/dart_config.git"
source: git
version: "0.5.0"
date_format:
dependency: "direct main"
description:
name: date_format
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
event_bus:
dependency: "direct main"
description:
@ -424,6 +431,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.3"
utf:
dependency: transitive
description:

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 0.1.1-alpha
version: 0.2.0+22
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -15,6 +15,8 @@ dependencies:
package_info: ^0.3.2
flutter_launcher_icons: ^0.6.1
cached_network_image: ^0.4.1
url_launcher: ^3.0.3
date_format: ^1.0.5
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.

View File

@ -12,7 +12,7 @@ import 'package:hass_client/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(new HassClientApp());
await tester.pumpWidget(new HAClientApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);