Compare commits
34 Commits
0.1.2-alph
...
0.2.0
Author | SHA1 | Date | |
---|---|---|---|
b89b5dfb98 | |||
a196b0d8d4 | |||
95f7c14296 | |||
2fcd27d240 | |||
6834f2ca34 | |||
c0a9b89d40 | |||
067ccfde02 | |||
4b4fc338f6 | |||
08c07e8398 | |||
df04d000b2 | |||
d0d1ab2740 | |||
af3a5bc611 | |||
b935a0e372 | |||
49444ab3df | |||
098a556279 | |||
375ae36884 | |||
0b42019ef3 | |||
516d38a8a9 | |||
fb886a4622 | |||
662b44d443 | |||
f9c48e6cc7 | |||
88d6e1008f | |||
4540fadf1e | |||
bd13d3693d | |||
5db9d6005f | |||
7e4f744598 | |||
772b569da5 | |||
0e11c1a146 | |||
60793dbf89 | |||
2b622cff04 | |||
94bcc30421 | |||
94f43ded6f | |||
7f7be8aa78 | |||
c0e0059487 |
@ -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
9
lib/badge_class.dart
Normal 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
25
lib/card_class.dart
Normal 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
314
lib/entity.class.dart
Normal 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
65
lib/entity.page.dart
Normal 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();
|
||||
}
|
||||
}
|
112
lib/entity_collection.class.dart
Normal file
112
lib/entity_collection.class.dart
Normal 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;
|
||||
}
|
||||
|
||||
}
|
260
lib/home_assistant.class.dart
Normal file
260
lib/home_assistant.class.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
),
|
||||
)
|
||||
);
|
400
lib/main.dart
400
lib/main.dart
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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
66
lib/ui_builder_class.dart
Normal 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
72
lib/utils.class.dart
Normal 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
53
lib/view_class.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
14
pubspec.lock
14
pubspec.lock
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user