Compare commits
56 Commits
v0.1.0-alp
...
0.2.2
Author | SHA1 | Date | |
---|---|---|---|
eee8f21e76 | |||
8ce3560d8d | |||
9e97bac85b | |||
4a0b447f00 | |||
bc4969dae8 | |||
5025b3d384 | |||
0d7e7eb6f7 | |||
062392b38c | |||
acd468ae75 | |||
60f216df13 | |||
9de8a659d3 | |||
7dd8f65af7 | |||
9e83a3e447 | |||
2f135169a9 | |||
76d2750ad6 | |||
571778fbd4 | |||
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 | |||
23d3d1839f | |||
aa0d7ee8fd | |||
36d727b454 | |||
7cad0141c7 | |||
86738a0515 | |||
4dc211f2f7 |
@ -39,8 +39,8 @@ android {
|
|||||||
applicationId "com.keyboardcrumbs.haclient"
|
applicationId "com.keyboardcrumbs.haclient"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 27
|
targetSdkVersion 27
|
||||||
versionCode 18
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName "0.1.0-alpha"
|
versionName flutterVersionName
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
57
lib/entity.page.dart
Normal file
57
lib/entity.page.dart
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class EntityViewPage extends StatefulWidget {
|
||||||
|
EntityViewPage({Key key, this.entity}) : super(key: key);
|
||||||
|
|
||||||
|
final Entity entity;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_EntityViewPageState createState() => new _EntityViewPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EntityViewPageState extends State<EntityViewPage> {
|
||||||
|
String _title;
|
||||||
|
Entity _entity;
|
||||||
|
StreamSubscription _stateSubscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_entity = widget.entity;
|
||||||
|
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||||
|
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||||
|
if (event.entityId == _entity.entityId) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_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: _entity.buildWidget(context, false)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose(){
|
||||||
|
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
24
lib/entity_class/button_entity.class.dart
Normal file
24
lib/entity_class/button_entity.class.dart
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class _ButtonEntityWidgetState extends _EntityWidgetState {
|
||||||
|
|
||||||
|
@override
|
||||||
|
void sendNewState(newValue) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "turn_on", widget.entity.entityId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||||
|
return FlatButton(
|
||||||
|
onPressed: (() {
|
||||||
|
sendNewState(null);
|
||||||
|
}),
|
||||||
|
child: Text(
|
||||||
|
"EXECUTE",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style:
|
||||||
|
new TextStyle(fontSize: Entity.STATE_FONT_SIZE, color: Colors.blue),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
92
lib/entity_class/datetime_entity.class.dart
Normal file
92
lib/entity_class/datetime_entity.class.dart
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class _DateTimeEntityWidgetState extends _EntityWidgetState {
|
||||||
|
bool get hasDate => widget.entity._attributes["has_date"] ?? false;
|
||||||
|
bool get hasTime => widget.entity._attributes["has_time"] ?? false;
|
||||||
|
int get year => widget.entity._attributes["year"] ?? 1970;
|
||||||
|
int get month => widget.entity._attributes["month"] ?? 1;
|
||||||
|
int get day => widget.entity._attributes["day"] ?? 1;
|
||||||
|
int get hour => widget.entity._attributes["hour"] ?? 0;
|
||||||
|
int get minute => widget.entity._attributes["minute"] ?? 0;
|
||||||
|
int get second => widget.entity._attributes["second"] ?? 0;
|
||||||
|
String get formattedState => _getFormattedState();
|
||||||
|
DateTime get dateTimeState => _getDateTimeState();
|
||||||
|
|
||||||
|
DateTime _getDateTimeState() {
|
||||||
|
return DateTime(this.year, this.month, this.day, this.hour, this.minute, this.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getFormattedState() {
|
||||||
|
String formattedState = "";
|
||||||
|
if (this.hasDate) {
|
||||||
|
formattedState += formatDate(dateTimeState, [M, ' ', d, ', ', yyyy]);
|
||||||
|
}
|
||||||
|
if (this.hasTime) {
|
||||||
|
formattedState += " "+formatDate(dateTimeState, [HH, ':', nn]);
|
||||||
|
}
|
||||||
|
return formattedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void sendNewState(newValue) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_datetime", widget.entity.entityId,
|
||||||
|
newValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding:
|
||||||
|
EdgeInsets.fromLTRB(0.0, 0.0, Entity.RIGHT_WIDGET_PADDING, 0.0),
|
||||||
|
child: GestureDetector(
|
||||||
|
child: Text(
|
||||||
|
"$formattedState",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: new TextStyle(
|
||||||
|
fontSize: Entity.STATE_FONT_SIZE,
|
||||||
|
)),
|
||||||
|
onTap: () => _handleStateTap(context),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleStateTap(BuildContext context) {
|
||||||
|
if (hasDate) {
|
||||||
|
_showDatePicker(context).then((date) {
|
||||||
|
if (date != null) {
|
||||||
|
if (hasTime) {
|
||||||
|
_showTimePicker(context).then((time){
|
||||||
|
sendNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}", "time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sendNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (hasTime) {
|
||||||
|
_showTimePicker(context).then((time){
|
||||||
|
if (time != null) {
|
||||||
|
sendNewState({"time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
TheLogger.log("Warning", "${widget.entity.entityId} has no date and no time");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _showDatePicker(BuildContext context) {
|
||||||
|
return showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: dateTimeState,
|
||||||
|
firstDate: DateTime(1970),
|
||||||
|
lastDate: DateTime(2037) //Unix timestamp will finish at Jan 19, 2038
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _showTimePicker(BuildContext context) {
|
||||||
|
return showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.fromDateTime(dateTimeState)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
240
lib/entity_class/entity.class.dart
Normal file
240
lib/entity_class/entity.class.dart
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
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 RIGHT_WIDGET_PADDING = 14.0;
|
||||||
|
static const LEFT_WIDGET_PADDING = 8.0;
|
||||||
|
static const EXTENDED_WIDGET_HEIGHT = 50.0;
|
||||||
|
static const WIDGET_HEIGHT = 34.0;
|
||||||
|
static const ICON_SIZE = 28.0;
|
||||||
|
static const STATE_FONT_SIZE = 16.0;
|
||||||
|
static const NAME_FONT_SIZE = 16.0;
|
||||||
|
static const SMALL_FONT_SIZE = 14.0;
|
||||||
|
static const INPUT_WIDTH = 160.0;
|
||||||
|
|
||||||
|
Map _attributes;
|
||||||
|
String _domain;
|
||||||
|
String _entityId;
|
||||||
|
String _state;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityWidget buildWidget(BuildContext context, bool inCard) {
|
||||||
|
return EntityWidget(
|
||||||
|
entity: this,
|
||||||
|
inCard: inCard,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getLastUpdatedFormatted() {
|
||||||
|
if (_lastUpdated == null) {
|
||||||
|
return "-";
|
||||||
|
} else {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
Duration d = now.difference(_lastUpdated);
|
||||||
|
String text;
|
||||||
|
int v;
|
||||||
|
if (d.inDays == 0) {
|
||||||
|
if (d.inHours == 0) {
|
||||||
|
if (d.inMinutes == 0) {
|
||||||
|
text = "seconds ago";
|
||||||
|
v = d.inSeconds;
|
||||||
|
} else {
|
||||||
|
text = "minutes ago";
|
||||||
|
v = d.inMinutes;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = "hours ago";
|
||||||
|
v = d.inHours;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = "days ago";
|
||||||
|
v = d.inDays;
|
||||||
|
}
|
||||||
|
return "$v $text";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntityWidget extends StatefulWidget {
|
||||||
|
EntityWidget({Key key, this.entity, this.inCard}) : super(key: key);
|
||||||
|
|
||||||
|
final Entity entity;
|
||||||
|
final bool inCard;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_EntityWidgetState createState() {
|
||||||
|
switch (entity.domain) {
|
||||||
|
case "automation":
|
||||||
|
case "input_boolean ":
|
||||||
|
case "switch":
|
||||||
|
case "light": {
|
||||||
|
return _SwitchEntityWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
case "script":
|
||||||
|
case "scene": {
|
||||||
|
return _ButtonEntityWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
case "input_datetime": {
|
||||||
|
return _DateTimeEntityWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
case "input_select": {
|
||||||
|
return _SelectEntityWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
case "input_number": {
|
||||||
|
return _SliderEntityWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
case "input_text": {
|
||||||
|
return _TextEntityWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return _EntityWidgetState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EntityWidgetState extends State<EntityWidget> {
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.inCard) {
|
||||||
|
return _buildMainWidget(context);
|
||||||
|
} else {
|
||||||
|
return ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
_buildMainWidget(context),
|
||||||
|
_buildLastUpdatedWidget()
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMainWidget(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: Entity.WIDGET_HEIGHT,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
GestureDetector(
|
||||||
|
child: _buildIconWidget(),
|
||||||
|
onTap: widget.inCard ? openEntityPage : null,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
child: _buildNameWidget(),
|
||||||
|
onTap: widget.inCard ? openEntityPage : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildActionWidget(widget.inCard, context)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void openEntityPage() {
|
||||||
|
eventBus.fire(new ShowEntityPageEvent(widget.entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendNewState(newState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildAdditionalWidget() {
|
||||||
|
return _buildLastUpdatedWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildIconWidget() {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Entity.LEFT_WIDGET_PADDING, 0.0, 12.0, 0.0),
|
||||||
|
child: MaterialDesignIcons.createIconWidgetFromEntityData(
|
||||||
|
widget.entity,
|
||||||
|
Entity.ICON_SIZE,
|
||||||
|
Entity.STATE_ICONS_COLORS[widget.entity.state] ?? Colors.blueGrey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLastUpdatedWidget() {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
Entity.LEFT_WIDGET_PADDING, Entity.SMALL_FONT_SIZE, 0.0, 0.0),
|
||||||
|
child: Text(
|
||||||
|
'${widget.entity.lastUpdated}',
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: Entity.SMALL_FONT_SIZE, color: Colors.black26),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNameWidget() {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(right: 10.0),
|
||||||
|
child: Text(
|
||||||
|
"${widget.entity.displayName}",
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
softWrap: false,
|
||||||
|
style: TextStyle(fontSize: Entity.NAME_FONT_SIZE),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding:
|
||||||
|
EdgeInsets.fromLTRB(0.0, 0.0, Entity.RIGHT_WIDGET_PADDING, 0.0),
|
||||||
|
child: GestureDetector(
|
||||||
|
child: Text(
|
||||||
|
"${widget.entity.state}${widget.entity.unitOfMeasurement}",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: new TextStyle(
|
||||||
|
fontSize: Entity.STATE_FONT_SIZE,
|
||||||
|
)),
|
||||||
|
onTap: openEntityPage,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
36
lib/entity_class/select_entity.class.dart
Normal file
36
lib/entity_class/select_entity.class.dart
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class _SelectEntityWidgetState extends _EntityWidgetState {
|
||||||
|
List<String> _listOptions = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void sendNewState(newValue) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "select_option", widget.entity.entityId,
|
||||||
|
{"option": "$newValue"}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||||
|
_listOptions.clear();
|
||||||
|
if (widget.entity._attributes["options"] != null) {
|
||||||
|
widget.entity._attributes["options"].forEach((value){
|
||||||
|
_listOptions.add(value.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
width: Entity.INPUT_WIDTH,
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
value: widget.entity.state,
|
||||||
|
items: this._listOptions.map((String value) {
|
||||||
|
return new DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: new Text(value),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (_) {
|
||||||
|
sendNewState(_);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
65
lib/entity_class/slider_entity.class.dart
Normal file
65
lib/entity_class/slider_entity.class.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class _SliderEntityWidgetState extends _EntityWidgetState {
|
||||||
|
int _multiplier = 1;
|
||||||
|
|
||||||
|
double get minValue => widget.entity._attributes["min"] ?? 0.0;
|
||||||
|
double get maxValue => widget.entity._attributes["max"] ?? 100.0;
|
||||||
|
double get valueStep => widget.entity._attributes["step"] ?? 1.0;
|
||||||
|
double get doubleState => double.tryParse(widget.entity.state) ?? 0.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void sendNewState(newValue) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_value", widget.entity.entityId,
|
||||||
|
{"value": "${newValue.toString()}"}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||||
|
if (valueStep < 1) {
|
||||||
|
_multiplier = 10;
|
||||||
|
} else if (valueStep < 0.1) {
|
||||||
|
_multiplier = 100;
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
width: 200.0,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Slider(
|
||||||
|
min: this.minValue * _multiplier,
|
||||||
|
max: this.maxValue * _multiplier,
|
||||||
|
value: (doubleState <= this.maxValue) &&
|
||||||
|
(doubleState >= this.minValue)
|
||||||
|
? doubleState * _multiplier
|
||||||
|
: this.minValue * _multiplier,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
widget.entity.state = (value.roundToDouble() / _multiplier).toString();
|
||||||
|
});
|
||||||
|
/*eventBus.fire(new StateChangedEvent(widget.entity.entityId,
|
||||||
|
(value.roundToDouble() / _multiplier).toString(), true));*/
|
||||||
|
},
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
sendNewState(value.roundToDouble() / _multiplier);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(right: Entity.RIGHT_WIDGET_PADDING),
|
||||||
|
child: Text("${widget.entity.state}${widget.entity.unitOfMeasurement}",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: new TextStyle(
|
||||||
|
fontSize: Entity.STATE_FONT_SIZE,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
26
lib/entity_class/switch_entity.class.dart
Normal file
26
lib/entity_class/switch_entity.class.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class _SwitchEntityWidgetState extends _EntityWidgetState {
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void sendNewState(newValue) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
widget.entity.domain, (newValue as bool) ? "turn_on" : "turn_off", widget.entity.entityId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||||
|
return Switch(
|
||||||
|
value: widget.entity.isOn,
|
||||||
|
onChanged: ((switchState) {
|
||||||
|
sendNewState(switchState);
|
||||||
|
widget.entity.state = switchState ? 'on' : 'off';
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
83
lib/entity_class/text_entity.class.dart
Normal file
83
lib/entity_class/text_entity.class.dart
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class _TextEntityWidgetState extends _EntityWidgetState {
|
||||||
|
String _tmpValue;
|
||||||
|
FocusNode _focusNode = FocusNode();
|
||||||
|
bool validValue = false;
|
||||||
|
|
||||||
|
int get valueMinLength => widget.entity._attributes["min"] ?? -1;
|
||||||
|
int get valueMaxLength => widget.entity._attributes["max"] ?? -1;
|
||||||
|
String get valuePattern => widget.entity._attributes["pattern"] ?? null;
|
||||||
|
bool get isTextField => widget.entity._attributes["mode"] == "text";
|
||||||
|
bool get isPasswordField => widget.entity._attributes["mode"] == "password";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_focusNode.addListener(_focusListener);
|
||||||
|
_tmpValue = widget.entity.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void sendNewState(newValue) {
|
||||||
|
if (validate(newValue)) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_value", widget.entity.entityId,
|
||||||
|
{"value": "$newValue"}));
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_tmpValue = widget.entity.state;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool validate(newValue) {
|
||||||
|
if (newValue is String) {
|
||||||
|
validValue = (newValue.length >= this.valueMinLength) &&
|
||||||
|
(this.valueMaxLength == -1 ||
|
||||||
|
(newValue.length <= this.valueMaxLength));
|
||||||
|
} else {
|
||||||
|
validValue = true;
|
||||||
|
}
|
||||||
|
return validValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _focusListener() {
|
||||||
|
if (!_focusNode.hasFocus && (_tmpValue != widget.entity.state)) {
|
||||||
|
sendNewState(_tmpValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||||
|
if (!_focusNode.hasFocus && (_tmpValue != widget.entity.state)) {
|
||||||
|
_tmpValue = widget.entity.state;
|
||||||
|
}
|
||||||
|
if (this.isTextField || this.isPasswordField) {
|
||||||
|
return Container(
|
||||||
|
width: Entity.INPUT_WIDTH,
|
||||||
|
child: TextField(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
obscureText: this.isPasswordField,
|
||||||
|
controller: new TextEditingController.fromValue(
|
||||||
|
new TextEditingValue(
|
||||||
|
text: _tmpValue,
|
||||||
|
selection:
|
||||||
|
new TextSelection.collapsed(offset: _tmpValue.length))),
|
||||||
|
onChanged: (value) {
|
||||||
|
_tmpValue = value;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
TheLogger.log("Warning", "Unsupported input mode for ${widget.entity.entityId}");
|
||||||
|
return super._buildActionWidget(inCard, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_focusNode.removeListener(_focusListener);
|
||||||
|
_focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
91
lib/entity_collection.class.dart
Normal file
91
lib/entity_collection.class.dart
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
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) {
|
||||||
|
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", "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;
|
||||||
|
}
|
||||||
|
}
|
61
lib/log.page.dart
Normal file
61
lib/log.page.dart
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class LogViewPage extends StatefulWidget {
|
||||||
|
LogViewPage({Key key, this.title}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LogViewPageState createState() => new _LogViewPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogViewPageState extends State<LogViewPage> {
|
||||||
|
String _logData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadLog() async {
|
||||||
|
_logData = TheLogger.getLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new Scaffold(
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
|
// Here we take the value from the MyHomePage object that was created by
|
||||||
|
// the App.build method, and use it to set our appbar title.
|
||||||
|
title: new Text(widget.title),
|
||||||
|
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: _logData
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
403
lib/main.dart
403
lib/main.dart
@ -8,19 +8,55 @@ import 'package:progress_indicators/progress_indicators.dart';
|
|||||||
import 'package:event_bus/event_bus.dart';
|
import 'package:event_bus/event_bus.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.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 'settings.dart';
|
part 'entity_class/entity.class.dart';
|
||||||
part 'data_model.dart';
|
part 'entity_class/button_entity.class.dart';
|
||||||
|
part 'entity_class/datetime_entity.class.dart';
|
||||||
|
part 'entity_class/select_entity.class.dart';
|
||||||
|
part 'entity_class/slider_entity.class.dart';
|
||||||
|
part 'entity_class/switch_entity.class.dart';
|
||||||
|
part 'entity_class/text_entity.class.dart';
|
||||||
|
|
||||||
|
part 'settings.page.dart';
|
||||||
|
part 'home_assistant.class.dart';
|
||||||
|
part 'log.page.dart';
|
||||||
|
part 'entity.page.dart';
|
||||||
|
part 'utils.class.dart';
|
||||||
|
part 'mdi.class.dart';
|
||||||
|
part 'entity_collection.class.dart';
|
||||||
|
part 'ui_builder_class.dart';
|
||||||
|
part 'view_class.dart';
|
||||||
|
part 'card_class.dart';
|
||||||
|
part 'badge_class.dart';
|
||||||
|
|
||||||
EventBus eventBus = new EventBus();
|
EventBus eventBus = new EventBus();
|
||||||
const String appName = "HA Client";
|
const String appName = "HA Client";
|
||||||
const appVersion = "0.1.0-alpha";
|
const appVersion = "0.2.2";
|
||||||
|
|
||||||
String homeAssistantWebHost;
|
String homeAssistantWebHost;
|
||||||
|
|
||||||
void main() => runApp(new HassClientApp());
|
void main() {
|
||||||
|
FlutterError.onError = (errorDetails) {
|
||||||
|
TheLogger.log("Error", "${errorDetails.exception}");
|
||||||
|
if (TheLogger.isInDebugMode) {
|
||||||
|
FlutterError.dumpErrorToConsole(errorDetails);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class HassClientApp extends StatelessWidget {
|
runZoned(() {
|
||||||
|
runApp(new HAClientApp());
|
||||||
|
}, onError: (error, stack) {
|
||||||
|
TheLogger.log("Global error", "$error");
|
||||||
|
if (TheLogger.isInDebugMode) {
|
||||||
|
debugPrint("$stack");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class HAClientApp extends StatelessWidget {
|
||||||
// This widget is the root of your application.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -31,8 +67,9 @@ class HassClientApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
initialRoute: "/",
|
initialRoute: "/",
|
||||||
routes: {
|
routes: {
|
||||||
"/": (context) => MainPage(title: 'Hass Client'),
|
"/": (context) => MainPage(title: 'HA Client'),
|
||||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Connection Settings")
|
"/connection-settings": (context) => ConnectionSettingsPage(title: "Connection Settings"),
|
||||||
|
"/log-view": (context) => LogViewPage(title: "Log")
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -48,24 +85,19 @@ class MainPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||||
HassioDataModel _dataModel;
|
HomeAssistant _homeAssistant;
|
||||||
Map _entitiesData;
|
EntityCollection _entities;
|
||||||
Map _uiStructure;
|
//Map _instanceConfig;
|
||||||
Map _instanceConfig;
|
|
||||||
int _uiViewsCount = 0;
|
int _uiViewsCount = 0;
|
||||||
String _instanceHost;
|
String _instanceHost;
|
||||||
int _errorCodeToBeShown = 0;
|
int _errorCodeToBeShown = 0;
|
||||||
String _lastErrorMessage = "";
|
String _lastErrorMessage = "";
|
||||||
StreamSubscription _stateSubscription;
|
StreamSubscription _stateSubscription;
|
||||||
StreamSubscription _settingsSubscription;
|
StreamSubscription _settingsSubscription;
|
||||||
|
StreamSubscription _serviceCallSubscription;
|
||||||
|
StreamSubscription _showEntityPageSubscription;
|
||||||
bool _isLoading = true;
|
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 = {
|
Map<String, Color> _badgeColors = {
|
||||||
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
||||||
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
||||||
@ -76,7 +108,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
|
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
|
||||||
debugPrint("Settings change event: reconnect=${event.reconnect}");
|
TheLogger.log("Debug","Settings change event: reconnect=${event.reconnect}");
|
||||||
setState(() {
|
setState(() {
|
||||||
_errorCodeToBeShown = 0;
|
_errorCodeToBeShown = 0;
|
||||||
});
|
});
|
||||||
@ -87,7 +119,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
debugPrint("$state");
|
TheLogger.log("Debug","$state");
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
_refreshData();
|
_refreshData();
|
||||||
}
|
}
|
||||||
@ -108,21 +140,33 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
_errorCodeToBeShown = 5;
|
_errorCodeToBeShown = 5;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (_dataModel != null) _dataModel.closeConnection();
|
if (_homeAssistant != null) _homeAssistant.closeConnection();
|
||||||
_createConnection(apiEndpoint, apiPassword, authType);
|
_createConnection(apiEndpoint, apiPassword, authType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_createConnection(String apiEndpoint, String apiPassword, String authType) {
|
_createConnection(String apiEndpoint, String apiPassword, String authType) {
|
||||||
_dataModel = HassioDataModel(apiEndpoint, apiPassword, authType);
|
_homeAssistant = HomeAssistant(apiEndpoint, apiPassword, authType);
|
||||||
_refreshData();
|
_refreshData();
|
||||||
if (_stateSubscription != null) _stateSubscription.cancel();
|
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||||
debugPrint("State change event for ${event.entityId}");
|
|
||||||
setState(() {
|
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 {
|
_refreshData() async {
|
||||||
@ -130,13 +174,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
});
|
});
|
||||||
_errorCodeToBeShown = 0;
|
_errorCodeToBeShown = 0;
|
||||||
if (_dataModel != null) {
|
if (_homeAssistant != null) {
|
||||||
await _dataModel.fetch().then((result) {
|
await _homeAssistant.fetch().then((result) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_instanceConfig = _dataModel.instanceConfig;
|
//_instanceConfig = _homeAssistant.instanceConfig;
|
||||||
_entitiesData = _dataModel.entities;
|
_entities = _homeAssistant.entities;
|
||||||
_uiStructure = _dataModel.uiStructure;
|
_uiViewsCount = _homeAssistant.viewsCount;
|
||||||
_uiViewsCount = _uiStructure.length;
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
@ -153,27 +196,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(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
});
|
});
|
||||||
_dataModel.callService(domain, service, entityId).then((r) {
|
_homeAssistant.callService(domain, service, entityId, additionalParams).then((r) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}).catchError((e) => _setErrorState(e));
|
}).catchError((e) => _setErrorState(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showEntityPage(Entity entity) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => EntityViewPage(entity: entity),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
List<Widget> _buildViews() {
|
List<Widget> _buildViews() {
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if ((_entitiesData != null) && (_uiStructure != null)) {
|
if ((_entities != null) && (!_homeAssistant.uiBuilder.isEmpty)) {
|
||||||
_uiStructure.forEach((viewId, structure) {
|
_homeAssistant.uiBuilder.views.forEach((viewId, view) {
|
||||||
result.add(
|
result.add(
|
||||||
RefreshIndicator(
|
RefreshIndicator(
|
||||||
color: Colors.amber,
|
color: Colors.amber,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
children: _buildSingleView(structure),
|
children: _buildSingleView(view),
|
||||||
),
|
),
|
||||||
onRefresh: () => _refreshData(),
|
onRefresh: () => _refreshData(),
|
||||||
)
|
)
|
||||||
@ -183,54 +235,49 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildSingleView(structure) {
|
List<Widget> _buildSingleView(View view) {
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (structure["badges"]["children"].length > 0) {
|
if (view.isThereBadges) {
|
||||||
result.add(
|
result.add(
|
||||||
Wrap(
|
Wrap(
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
spacing: 10.0,
|
spacing: 10.0,
|
||||||
runSpacing: 4.0,
|
runSpacing: 1.0,
|
||||||
//padding: new EdgeInsets.all(8.0),
|
children: _buildBadges(view.badges),
|
||||||
//itemExtent: 40.0,
|
)
|
||||||
children: _buildBadges(structure["badges"]["children"]),
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
structure["groups"].forEach((id, group) {
|
view.cards.forEach((id, card) {
|
||||||
if (group["children"].length > 0) {
|
if (card.entities.isNotEmpty) {
|
||||||
result.add(_buildCard(
|
result.add(_buildCard(card));
|
||||||
group["children"], group["friendly_name"].toString()));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildBadges(List ids) {
|
List<Widget> _buildBadges( Map<String, Badge> badges) {
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
ids.forEach((entityId) {
|
badges.forEach((id, badge) {
|
||||||
var data = _entitiesData[entityId];
|
var badgeEntity = _entities.get(id);
|
||||||
if (data == null) {
|
if (badgeEntity != null) {
|
||||||
debugPrint("Hiding unknown entity from badges: $entityId");
|
|
||||||
} else {
|
|
||||||
result.add(
|
result.add(
|
||||||
_buildSingleBadge(data)
|
_buildSingleBadge(badgeEntity)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSingleBadge(data) {
|
Widget _buildSingleBadge(Entity data) {
|
||||||
double iconSize = 26.0;
|
double iconSize = 26.0;
|
||||||
Widget badgeIcon;
|
Widget badgeIcon;
|
||||||
String badgeTextValue;
|
String badgeTextValue;
|
||||||
Color iconColor = _badgeColors[data["domain"]] ?? _badgeColors["default"];
|
Color iconColor = _badgeColors[data.domain] ?? _badgeColors["default"];
|
||||||
switch (data["domain"]) {
|
switch (data.domain) {
|
||||||
case "sun": {
|
case "sun": {
|
||||||
badgeIcon = data["state"] == "below_horizon" ?
|
badgeIcon = data.state == "below_horizon" ?
|
||||||
Icon(
|
Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
|
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
@ -242,35 +289,35 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "sensor": {
|
case "sensor": {
|
||||||
badgeTextValue = data["attributes"]["unit_of_measurement"];
|
badgeTextValue = data.unitOfMeasurement;
|
||||||
badgeIcon = Center(
|
badgeIcon = Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"${data['state']}",
|
"${data.state == 'unknown' ? '-' : data.state}",
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: 18.0),
|
style: TextStyle(fontSize: 17.0),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "device_tracker": {
|
case "device_tracker": {
|
||||||
badgeIcon = MaterialDesignIcons.createIconFromEntityData(data, iconSize,Colors.black);
|
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(data, iconSize,Colors.black);
|
||||||
badgeTextValue = data["state"];
|
badgeTextValue = data.state;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
badgeIcon = MaterialDesignIcons.createIconFromEntityData(data, iconSize,Colors.black);
|
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(data, iconSize,Colors.black);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Widget badgeText;
|
Widget badgeText;
|
||||||
if (badgeTextValue == null) {
|
if (badgeTextValue == null || badgeTextValue.length == 0) {
|
||||||
badgeText = Container(width: 0.0, height: 0.0);
|
badgeText = Container(width: 0.0, height: 0.0);
|
||||||
} else {
|
} else {
|
||||||
badgeText = Container(
|
badgeText = Container(
|
||||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||||
child: Text("$badgeTextValue",
|
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),
|
textAlign: TextAlign.center, softWrap: false, overflow: TextOverflow.fade),
|
||||||
decoration: new BoxDecoration(
|
decoration: new BoxDecoration(
|
||||||
// Circle shape
|
// Circle shape
|
||||||
@ -309,8 +356,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
Positioned(
|
Positioned(
|
||||||
//width: 50.0,
|
//width: 50.0,
|
||||||
bottom: -9.0,
|
bottom: -9.0,
|
||||||
left: -15.0,
|
left: -10.0,
|
||||||
right: -15.0,
|
right: -10.0,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: badgeText,
|
child: badgeText,
|
||||||
)
|
)
|
||||||
@ -321,10 +368,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
Container(
|
Container(
|
||||||
width: 60.0,
|
width: 60.0,
|
||||||
child: Text(
|
child: Text(
|
||||||
"${data['display_name']}",
|
"${data.displayName}",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 12.0),
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
maxLines: 2,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -332,18 +380,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Card _buildCard(List ids, String name) {
|
Card _buildCard(HACard card) {
|
||||||
List<Widget> body = [];
|
List<Widget> body = [];
|
||||||
body.add(_buildCardHeader(name));
|
body.add(_buildCardHeader(card.friendlyName));
|
||||||
body.addAll(_buildCardBody(ids));
|
body.addAll(_buildCardBody(card.entities));
|
||||||
Card result =
|
Card result = Card(
|
||||||
Card(child: new Column(mainAxisSize: MainAxisSize.min, children: body));
|
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCardHeader(String name) {
|
Widget _buildCardHeader(String name) {
|
||||||
var result;
|
var result;
|
||||||
if (name.length > 0) {
|
if (name.trim().length > 0) {
|
||||||
result = new ListTile(
|
result = new ListTile(
|
||||||
//leading: const Icon(Icons.device_hub),
|
//leading: const Icon(Icons.device_hub),
|
||||||
//subtitle: Text(".."),
|
//subtitle: Text(".."),
|
||||||
@ -362,92 +411,29 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
List<Widget> _buildCardBody(List ids) {
|
List<Widget> _buildCardBody(List ids) {
|
||||||
List<Widget> entities = [];
|
List<Widget> entities = [];
|
||||||
ids.forEach((id) {
|
ids.forEach((id) {
|
||||||
var data = _entitiesData[id];
|
var entity = _entities.get(id);
|
||||||
if (data == null) {
|
if (entity != null) {
|
||||||
debugPrint("Hiding unknown entity from card: $id");
|
entities.add(
|
||||||
} else {
|
Padding(
|
||||||
entities.add(new ListTile(
|
padding: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
|
||||||
leading: MaterialDesignIcons.createIconFromEntityData(data, 28.0, _stateIconColors[data["state"]] ?? Colors.blueGrey),
|
child: entity.buildWidget(context, true),
|
||||||
//subtitle: Text("${data['entity_id']}"),
|
));
|
||||||
trailing: _buildEntityActionWidget(data),
|
|
||||||
title: Text(
|
|
||||||
"${data["display_name"]}",
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
softWrap: false,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return entities;
|
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> buildUIViewTabs() {
|
||||||
List<Tab> result = [];
|
List<Tab> result = [];
|
||||||
if ((_entitiesData != null) && (_uiStructure != null)) {
|
if ((_entities != null) && (!_homeAssistant.uiBuilder.isEmpty)) {
|
||||||
_uiStructure.forEach((viewId, structure) {
|
_homeAssistant.uiBuilder.views.forEach((viewId, view) {
|
||||||
result.add(
|
result.add(
|
||||||
Tab(
|
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,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -457,7 +443,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
Widget _buildAppTitle() {
|
Widget _buildAppTitle() {
|
||||||
Row titleRow = Row(
|
Row titleRow = Row(
|
||||||
children: [Text(_instanceConfig != null ? _instanceConfig["location_name"] : "")],
|
children: [Text(_homeAssistant != null ? _homeAssistant.locationName : "")],
|
||||||
);
|
);
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
titleRow.children.add(Padding(
|
titleRow.children.add(Padding(
|
||||||
@ -476,7 +462,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new UserAccountsDrawerHeader(
|
new UserAccountsDrawerHeader(
|
||||||
accountName: Text(_instanceConfig != null ? _instanceConfig["location_name"] : "Unknown"),
|
accountName: Text(_homeAssistant != null ? _homeAssistant.locationName : "Unknown"),
|
||||||
accountEmail: Text(_instanceHost ?? "Not configured"),
|
accountEmail: Text(_instanceHost ?? "Not configured"),
|
||||||
currentAccountPicture: new Image.asset('images/hassio-192x192.png'),
|
currentAccountPicture: new Image.asset('images/hassio-192x192.png'),
|
||||||
),
|
),
|
||||||
@ -484,7 +470,24 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
leading: Icon(Icons.settings),
|
leading: Icon(Icons.settings),
|
||||||
title: Text("Connection settings"),
|
title: Text("Connection settings"),
|
||||||
onTap: () {
|
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.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(
|
new AboutListTile(
|
||||||
@ -575,62 +578,44 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_checkShowInfo(context);
|
_checkShowInfo(context);
|
||||||
// This method is rerun every time setState is called.
|
// This method is rerun every time setState is called.
|
||||||
//
|
if (_entities == null) {
|
||||||
if (_entitiesData == null) {
|
return _buildScaffold(true);
|
||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: _uiViewsCount,
|
length: _uiViewsCount,
|
||||||
child: new Scaffold(
|
child: _buildScaffold(false)
|
||||||
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()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -640,7 +625,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
if (_stateSubscription != null) _stateSubscription.cancel();
|
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||||
if (_settingsSubscription != null) _settingsSubscription.cancel();
|
if (_settingsSubscription != null) _settingsSubscription.cancel();
|
||||||
_dataModel.closeConnection();
|
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
|
||||||
|
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
|
||||||
|
_homeAssistant.closeConnection();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,341 +1,5 @@
|
|||||||
part of 'main.dart';
|
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)) {
|
|
||||||
debugPrint("Previous fetch is not complited");
|
|
||||||
} else {
|
|
||||||
//TODO: Fetch timeout timer. Should be removed after #21 fix
|
|
||||||
_fetchingTimer = Timer(Duration(seconds: 10), () {
|
|
||||||
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)) {
|
|
||||||
debugPrint("Socket connecting...");
|
|
||||||
_hassioChannel = IOWebSocketChannel.connect(_hassioAPIEndpoint);
|
|
||||||
_hassioChannel.stream.handleError((e) {
|
|
||||||
debugPrint("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);
|
|
||||||
debugPrint("[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) {
|
|
||||||
debugPrint("Request id:$_currentMessageId was successful");
|
|
||||||
} else {
|
|
||||||
debugPrint("Skipped message due to messageId:");
|
|
||||||
debugPrint(message);
|
|
||||||
}
|
|
||||||
} else if (data["type"] == "event") {
|
|
||||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
|
||||||
_handleEntityStateChange(data["event"]["data"]);
|
|
||||||
} else if (data["event"] != null) {
|
|
||||||
debugPrint("Unhandled event type: ${data["event"]["event_type"]}");
|
|
||||||
} else {
|
|
||||||
debugPrint("Event is null");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debugPrint("Unknown message type");
|
|
||||||
debugPrint(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(message) {
|
|
||||||
debugPrint("[Sent]$message");
|
|
||||||
_hassioChannel.sink.add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleEntityStateChange(Map eventData) {
|
|
||||||
String entityId = eventData["entity_id"];
|
|
||||||
if (_entitiesData[entityId] != null) {
|
|
||||||
_entitiesData[entityId].addAll(eventData["new_state"]);
|
|
||||||
eventBus.fire(new StateChangedEvent(eventData["entity_id"]));
|
|
||||||
} else {
|
|
||||||
debugPrint("Unknown enity $entityId");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
Map data = response["result"];
|
|
||||||
Map result = {};
|
|
||||||
debugPrint("Parsing ${data.length} Home Assistant service domains");
|
|
||||||
data.forEach((domain, services){
|
|
||||||
result[domain] = Map.from(services);
|
|
||||||
services.forEach((serviceName, serviceData){
|
|
||||||
if (_entitiesData["$domain.$serviceName"] != null) {
|
|
||||||
result[domain].remove(serviceName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
_servicesData = result;
|
|
||||||
_servicesCompleter.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseEntities(response) async {
|
|
||||||
if (response["success"] == false) {
|
|
||||||
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
List data = response["result"];
|
|
||||||
debugPrint("Parsing ${data.length} Home Assistant entities");
|
|
||||||
List<String> uiGroups = [];
|
|
||||||
data.forEach((entity) {
|
|
||||||
var composedEntity = Map.from(entity);
|
|
||||||
String entityDomain = entity["entity_id"].split(".")[0];
|
|
||||||
String entityId = entity["entity_id"];
|
|
||||||
|
|
||||||
composedEntity["display_name"] = "${entity["attributes"]!=null ? entity["attributes"]["friendly_name"] ?? entity["attributes"]["name"] : "_"}";
|
|
||||||
composedEntity["domain"] = entityDomain;
|
|
||||||
|
|
||||||
if (composedEntity["attributes"] != null) {
|
|
||||||
if ((entityDomain == "group")&&(composedEntity["attributes"]["view"] == true)) {
|
|
||||||
uiGroups.add(entityId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (entityDomain == "group") {
|
|
||||||
if ((composedEntity["attributes"] != null) &&
|
|
||||||
(composedEntity["attributes"]["view"] == true)) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_entitiesData[entityId] = Map.from(composedEntity);
|
|
||||||
});
|
|
||||||
|
|
||||||
//Gethering information for UI
|
|
||||||
debugPrint("Gethering views");
|
|
||||||
int viewCounter = 0;
|
|
||||||
uiGroups.forEach((viewId) { //Each view
|
|
||||||
viewCounter +=1;
|
|
||||||
var viewGroup = _entitiesData[viewId];
|
|
||||||
Map viewGroupStructure = {};
|
|
||||||
if (viewGroup != null) {
|
|
||||||
viewGroupStructure["groups"] = {};
|
|
||||||
viewGroupStructure["state"] = "on";
|
|
||||||
viewGroupStructure["entity_id"] = viewGroup["entity_id"];
|
|
||||||
viewGroupStructure["badges"] = {"children": []};
|
|
||||||
viewGroupStructure["attributes"] = viewGroup["attributes"] != null ? {"icon": viewGroup["attributes"]["icon"]} : {"icon": "none"};
|
|
||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_statesCompleter.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
class MaterialDesignIcons {
|
||||||
static Map _defaultIconsByDomains = {
|
static Map _defaultIconsByDomains = {
|
||||||
"light": "mdi:lightbulb",
|
"light": "mdi:lightbulb",
|
||||||
@ -350,7 +14,9 @@ class MaterialDesignIcons {
|
|||||||
"input_number": "mdi:ray-vertex",
|
"input_number": "mdi:ray-vertex",
|
||||||
"input_select": "mdi:format-list-bulleted",
|
"input_select": "mdi:format-list-bulleted",
|
||||||
"input_text": "mdi:textbox",
|
"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 = {
|
static Map _defaultIconsByDeviceClass = {
|
||||||
@ -3202,35 +2868,35 @@ class MaterialDesignIcons {
|
|||||||
"mdi:blank": 0xf68c
|
"mdi:blank": 0xf68c
|
||||||
};
|
};
|
||||||
|
|
||||||
static Widget createIconFromEntityData(Map data, double size, Color color) {
|
static Widget createIconWidgetFromEntityData(Entity data, double size, Color color) {
|
||||||
if ((data["attributes"] != null) && (data["attributes"]["entity_picture"] != null)) {
|
if (data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (data.entityPicture != null) {
|
||||||
if (homeAssistantWebHost != null) {
|
if (homeAssistantWebHost != null) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
|
radius: size/2,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
backgroundImage: CachedNetworkImageProvider(
|
||||||
"$homeAssistantWebHost${data["attributes"]["entity_picture"]}",
|
"$homeAssistantWebHost${data.entityPicture}",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(width: 0.0, height: 0.0);
|
return Container(width: 0.0, height: 0.0);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String iconName = data["attributes"] != null
|
String iconName = data.icon;
|
||||||
? data["attributes"]["icon"]
|
|
||||||
: null;
|
|
||||||
int iconCode = 0;
|
int iconCode = 0;
|
||||||
if (iconName != null) {
|
if (iconName.length > 0) {
|
||||||
iconCode = getIconCodeByIconName(iconName);
|
iconCode = getIconCodeByIconName(iconName);
|
||||||
} else {
|
} else {
|
||||||
iconCode = getDefaultIconByEntityId(data["entity_id"],
|
iconCode = getDefaultIconByEntityId(data.entityId,
|
||||||
data["attributes"] != null
|
data.deviceClass, data.state); //
|
||||||
? data["attributes"]["device_class"]
|
|
||||||
: null, data["state"]); //
|
|
||||||
}
|
}
|
||||||
return Icon(
|
return Icon(
|
||||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||||
size: size,
|
size: size,
|
||||||
color: color,
|
color: color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -35,6 +35,9 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_saveSettings() async {
|
_saveSettings() async {
|
||||||
|
if (_hassioDomain.indexOf("http") == 0 && _hassioDomain.indexOf("//") > 0) {
|
||||||
|
_hassioDomain = _hassioDomain.split("//")[1];
|
||||||
|
}
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
prefs.setString("hassio-domain", _hassioDomain);
|
prefs.setString("hassio-domain", _hassioDomain);
|
||||||
prefs.setString("hassio-port", _hassioPort);
|
prefs.setString("hassio-port", _hassioPort);
|
||||||
@ -54,8 +57,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
});
|
});
|
||||||
eventBus.fire(SettingsChangedEvent(true));
|
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),
|
title: new Text(widget.title),
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
@ -63,7 +64,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new Row(
|
new Row(
|
||||||
children: [
|
children: [
|
||||||
Text("HTTPS"),
|
Text("Use ssl (HTTPS)"),
|
||||||
Switch(
|
Switch(
|
||||||
value: (_socketProtocol == "wss"),
|
value: (_socketProtocol == "wss"),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -129,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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
18
pubspec.lock
18
pubspec.lock
@ -87,6 +87,13 @@ packages:
|
|||||||
url: "https://github.com/MarkOSullivan94/dart_config.git"
|
url: "https://github.com/MarkOSullivan94/dart_config.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.5.0"
|
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:
|
event_bus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -278,7 +285,7 @@ packages:
|
|||||||
name: petitparser
|
name: petitparser
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.2"
|
||||||
plugin:
|
plugin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -424,6 +431,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.6"
|
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:
|
utf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -472,7 +486,7 @@ packages:
|
|||||||
name: xml
|
name: xml
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.2.3"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
name: hass_client
|
name: hass_client
|
||||||
description: Home Assistant Android Client
|
description: Home Assistant Android Client
|
||||||
|
|
||||||
version: 0.1.0-alpha
|
version: 0.2.2+24
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
||||||
@ -15,6 +15,8 @@ dependencies:
|
|||||||
package_info: ^0.3.2
|
package_info: ^0.3.2
|
||||||
flutter_launcher_icons: ^0.6.1
|
flutter_launcher_icons: ^0.6.1
|
||||||
cached_network_image: ^0.4.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.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
@ -12,7 +12,7 @@ import 'package:hass_client/main.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||||
// Build our app and trigger a frame.
|
// Build our app and trigger a frame.
|
||||||
await tester.pumpWidget(new HassClientApp());
|
await tester.pumpWidget(new HAClientApp());
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
// Verify that our counter starts at 0.
|
||||||
expect(find.text('0'), findsOneWidget);
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
Reference in New Issue
Block a user