Compare commits

...

56 Commits
0.2.1 ... 0.3.0

Author SHA1 Message Date
758376a891 Version 0.3.0 2018-10-16 17:53:50 +03:00
2ebba364e3 Resolves #76 Covers support 2018-10-16 17:35:13 +03:00
6e604440c0 Resolves #106 Climate support 2018-10-16 15:14:54 +03:00
c23034688e WIP #106 2018-10-15 18:04:16 +03:00
69f45b52cf WIP #106 2018-10-15 00:29:40 +03:00
ffc053fbe6 Full ui structure refactoring. InheritedWidget as entity model 2018-10-15 00:15:09 +03:00
b5f9ecf601 Minor fixes 2018-10-12 18:03:27 +03:00
948d1d4e23 Resolves #106 Climate support 2018-10-11 23:02:05 +03:00
136297c18b Climate default icon. Icon colors fix 2018-10-08 23:30:09 +03:00
164800951d Resolves #129 2018-10-08 23:11:56 +03:00
84d283de2b VIP #120 2018-10-07 23:06:06 +03:00
2fa35d771a Resolves #123 Account details and settings. Get user name from HA 2018-10-07 20:18:14 +03:00
326cd073b9 Async data fetching 2018-10-07 18:27:10 +03:00
e99c3f5742 Fix wrong password issue and infinity reconnects issue 2018-10-07 18:21:55 +03:00
16a9392fa6 Resolves #79 Too many tabs issue 2018-10-07 17:16:24 +03:00
5bf063969b Resolves #128 Enpty settings change issue 2018-10-07 17:07:06 +03:00
c19a0511a6 Version 0.2.5 2018-10-07 15:08:50 +03:00
a4ac40b366 Resolves #107 Show entity attributes 2018-10-07 15:03:51 +03:00
ce69f044fb Resolves #110: Slider improvements 2018-10-07 12:40:45 +03:00
70b6469bd1 Resolves #118 Fix message queue issue 2018-10-07 12:14:48 +03:00
253316fb1f TODOs 2018-10-07 10:41:41 +03:00
ec71200ab0 Resolves #127 Fix entities order in card 2018-10-07 10:36:50 +03:00
bc1f4eab2e Showing error snakbar improvements. Error icon in header 2018-10-07 10:28:28 +03:00
4085006446 Fix save settings issue 2018-10-07 09:55:37 +03:00
b7fb821abe View now a stateful widget to prevent memory leeks 2018-10-07 09:45:04 +03:00
284e7ba451 Resolves #125 UI building refactored 2018-10-07 02:17:14 +03:00
17a3bd8d35 Resolves #126 Connection settings save button 2018-10-06 20:03:20 +03:00
c2b88c8a12 Resolves #124: Connection handling improvements 2018-10-06 16:01:38 +03:00
c975af4c79 Unnecessary dependency removed 2018-10-03 21:50:11 +03:00
debf1b71f1 Remove some debug messages 2018-10-03 21:42:28 +03:00
4725953b32 Add entity widget type. Preparing to make entity build it's own badge 2018-10-03 16:44:11 +03:00
e7ca1209e2 Update app icon 2018-10-03 16:15:09 +03:00
f9afa663f5 Version code change 2018-10-03 15:55:48 +03:00
5068cbbcf4 Menu quick fix 2018-10-03 15:55:11 +03:00
043d3a9905 Changing only version code 2018-10-03 15:26:46 +03:00
77c5f80c13 Fix fetch timeout on app start 2018-10-03 15:25:01 +03:00
e0d35d07dc Version 0.2.4 2018-10-03 14:37:54 +03:00
285447a5b7 Resolves #114 Error going back from settings 2018-10-03 14:36:23 +03:00
ed3e4ba272 COnnection closing improvements 2018-10-03 10:35:40 +03:00
908563063a Fix input_boolean control 2018-10-03 09:50:14 +03:00
7f2611b410 Version 0.2.3 2018-10-03 00:55:50 +03:00
648750655c Resolves #109 No static width for inputs 2018-10-02 23:21:50 +03:00
8a0d5581d9 Resolves #111: Assumed state 2018-10-02 23:10:40 +03:00
98d716109b Resolves #21: Handling socket disconnect by sink done Future 2018-10-02 22:48:47 +03:00
ebb2f2b4e5 Decline all timeouts as variables 2018-10-02 18:05:50 +03:00
d910e4dd43 Add socket ping interval 2018-10-02 17:42:06 +03:00
95d80fbbfc Resolves #58: Message queue 2018-10-02 17:23:19 +03:00
41297150c2 Implement fetch timer with 30 timeout along with connection timer 2018-10-02 16:00:55 +03:00
b14b248f2f Resolves #72 reconnect on message sending 2018-10-02 15:46:24 +03:00
13fc1bff27 Resolves #61: Prevent second connection opening 2018-10-02 14:50:42 +03:00
eee8f21e76 Version 0.2.2 2018-10-02 00:48:25 +03:00
8ce3560d8d Merge pull request #108 from estevez-dev/feature/entity_widget
Refactoring: Stateful entity widgets
2018-10-01 21:44:31 +00:00
9e97bac85b Refactoring: Stateful entity widgets 2018-10-02 00:41:40 +03:00
4a0b447f00 Separate entity classes on different files 2018-10-01 21:57:54 +03:00
bc4969dae8 Resolves #104: wrong value is set for input_text 2018-10-01 10:24:38 +03:00
5025b3d384 Merge pull request #101 from estevez-dev/release/0.2.1
Release/0.2.1
2018-09-30 20:29:12 +00:00
40 changed files with 2960 additions and 1143 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 B

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

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

View File

@ -1,25 +1,54 @@
part of 'main.dart'; part of 'main.dart';
class HACard { class HACard extends StatelessWidget {
String _entityId;
List _entities;
String _friendlyName;
List get entities => _entities; final List<Entity> entities;
String get friendlyName => _friendlyName; final String friendlyName;
HACard(String groupId, String friendlyName) { const HACard({
_entityId = groupId; Key key,
_entities = []; this.entities,
_friendlyName = friendlyName; this.friendlyName
}) : super(key: key);
@override
Widget build(BuildContext context) {
List<Widget> body = [];
body.add(_buildCardHeader());
body.addAll(_buildCardBody(context));
return Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
);
} }
void addEntity(String entityId) { Widget _buildCardHeader() {
_entities.add(entityId); var result;
if ((friendlyName != null) && (friendlyName.trim().length > 0)) {
result = new ListTile(
//leading: const Icon(Icons.device_hub),
//subtitle: Text(".."),
//trailing: Text("${data["state"]}"),
title: Text("$friendlyName",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: 25.0)),
);
} else {
result = new Container(width: 0.0, height: 0.0);
}
return result;
} }
void addEntities(List entities) { List<Widget> _buildCardBody(BuildContext context) {
_entities.addAll(entities); List<Widget> result = [];
entities.forEach((Entity entity) {
result.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
child: entity.buildDefaultWidget(context),
));
});
return result;
} }
} }

View File

@ -1,486 +0,0 @@
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"]);
}
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";
}
}
void openEntityPage() {
eventBus.fire(new ShowEntityPageEvent(this));
}
void sendNewState(newState) {
return;
}
Widget buildWidget(bool inCard, BuildContext context) {
return SizedBox(
height: Entity.WIDGET_HEIGHT,
child: Row(
children: <Widget>[
GestureDetector(
child: _buildIconWidget(),
onTap: inCard ? openEntityPage : null,
),
Expanded(
child: GestureDetector(
child: _buildNameWidget(),
onTap: inCard ? openEntityPage : null,
),
),
_buildActionWidget(inCard, context)
],
),
);
}
Widget buildAdditionalWidget() {
return _buildLastUpdatedWidget();
}
Widget _buildIconWidget() {
return Padding(
padding: EdgeInsets.fromLTRB(Entity.LEFT_WIDGET_PADDING, 0.0, 12.0, 0.0),
child: MaterialDesignIcons.createIconWidgetFromEntityData(
this,
Entity.ICON_SIZE,
Entity.STATE_ICONS_COLORS[_state] ?? Colors.blueGrey),
);
}
Widget _buildLastUpdatedWidget() {
return Padding(
padding: EdgeInsets.fromLTRB(
Entity.LEFT_WIDGET_PADDING, Entity.SMALL_FONT_SIZE, 0.0, 0.0),
child: Text(
'${this.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(
"${this.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(
"$_state${this.unitOfMeasurement}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.STATE_FONT_SIZE,
)),
onTap: openEntityPage,
)
);
}
}
class SwitchEntity extends Entity {
SwitchEntity(Map rawData) : super(rawData);
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(
_domain, (newValue as bool) ? "turn_on" : "turn_off", entityId, null));
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
return Switch(
value: this.isOn,
onChanged: ((switchState) {
sendNewState(switchState);
}),
);
}
}
class ButtonEntity extends Entity {
ButtonEntity(Map rawData) : super(rawData);
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(_domain, "turn_on", _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),
),
);
}
}
//
// SLIDER
//
class SliderEntity extends Entity {
int _multiplier = 1;
double get minValue => _attributes["min"] ?? 0.0;
double get maxValue => _attributes["max"] ?? 100.0;
double get valueStep => _attributes["step"] ?? 1.0;
double get doubleState => double.tryParse(_state) ?? 0.0;
SliderEntity(Map rawData) : super(rawData) {
if (valueStep < 1) {
_multiplier = 10;
} else if (valueStep < 0.1) {
_multiplier = 100;
}
}
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(_domain, "set_value", _entityId,
{"value": "${newValue.toString()}"}));
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
return Container(
width: 200.0,
child: Row(
children: <Widget>[
Expanded(
child: Slider(
min: this.minValue * _multiplier,
max: this.maxValue * _multiplier,
value: (this.doubleState <= this.maxValue) &&
(this.doubleState >= this.minValue)
? this.doubleState * _multiplier
: this.minValue * _multiplier,
onChanged: (value) {
eventBus.fire(new StateChangedEvent(_entityId,
(value.roundToDouble() / _multiplier).toString(), true));
},
onChangeEnd: (value) {
sendNewState(value.roundToDouble() / _multiplier);
},
),
),
Padding(
padding: EdgeInsets.only(right: Entity.RIGHT_WIDGET_PADDING),
child: Text("$_state${this.unitOfMeasurement}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.STATE_FONT_SIZE,
)),
)
],
),
);
}
}
//
// DATETIME
//
class DateTimeEntity extends Entity {
bool get hasDate => _attributes["has_date"] ?? false;
bool get hasTime => _attributes["has_time"] ?? false;
int get year => _attributes["year"] ?? 1970;
int get month => _attributes["month"] ?? 1;
int get day => _attributes["day"] ?? 1;
int get hour => _attributes["hour"] ?? 0;
int get minute => _attributes["minute"] ?? 0;
int get second => _attributes["second"] ?? 0;
String get formattedState => _getFormattedState();
DateTime get dateTimeState => _getDateTimeState();
DateTimeEntity(Map rawData) : super(rawData);
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(_domain, "set_datetime", _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", "$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)
);
}
}
class SelectEntity extends Entity {
List<String> _listOptions = [];
String get initialValue => _attributes["initial"] ?? null;
SelectEntity(Map rawData) : super(rawData) {
if (_attributes["options"] != null) {
_attributes["options"].forEach((value){
_listOptions.add(value.toString());
});
}
}
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(_domain, "select_option", _entityId,
{"option": "$newValue"}));
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
return Container(
width: Entity.INPUT_WIDTH,
child: DropdownButton<String>(
value: _state,
items: this._listOptions.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (_) {
sendNewState(_);
},
),
);
}
}
class TextEntity extends Entity {
String tmpState;
FocusNode _focusNode;
bool validValue = false;
int get valueMinLength => _attributes["min"] ?? -1;
int get valueMaxLength => _attributes["max"] ?? -1;
String get valuePattern => _attributes["pattern"] ?? null;
bool get isTextField => _attributes["mode"] == "text";
bool get isPasswordField => _attributes["mode"] == "password";
TextEntity(Map rawData) : super(rawData) {
_focusNode = FocusNode();
//TODO possible memory leak generator
_focusNode.addListener(_focusListener);
//tmpState = state;
}
@override
void sendNewState(newValue) {
if (validate(newValue)) {
eventBus.fire(new ServiceCallEvent(_domain, "set_value", _entityId,
{"value": "{newValue"}));
}
}
@override
void update(Map rawData) {
super.update(rawData);
tmpState = _state;
}
bool validate(newValue) {
if (newValue is String) {
//TODO add pattern support
validValue = (newValue.length >= this.valueMinLength) &&
(this.valueMaxLength == -1 ||
(newValue.length <= this.valueMaxLength));
} else {
validValue = true;
}
return validValue;
}
void _focusListener() {
if (!_focusNode.hasFocus && (tmpState != state)) {
sendNewState(tmpState);
tmpState = state;
}
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
if (this.isTextField || this.isPasswordField) {
return Container(
width: Entity.INPUT_WIDTH,
child: TextField(
focusNode: inCard ? _focusNode : null,
obscureText: this.isPasswordField,
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: tmpState,
selection:
new TextSelection.collapsed(offset: tmpState.length))),
onChanged: (value) {
tmpState = value;
}),
);
} else {
TheLogger.log("Warning", "Unsupported input mode for $entityId");
return super._buildActionWidget(inCard, context);
}
}
}

View File

@ -1,9 +1,10 @@
part of 'main.dart'; part of 'main.dart';
class EntityViewPage extends StatefulWidget { class EntityViewPage extends StatefulWidget {
EntityViewPage({Key key, this.entity}) : super(key: key); EntityViewPage({Key key, @required this.entity, @required this.homeAssistant }) : super(key: key);
Entity entity; final Entity entity;
final HomeAssistant homeAssistant;
@override @override
_EntityViewPageState createState() => new _EntityViewPageState(); _EntityViewPageState createState() => new _EntityViewPageState();
@ -11,26 +12,34 @@ class EntityViewPage extends StatefulWidget {
class _EntityViewPageState extends State<EntityViewPage> { class _EntityViewPageState extends State<EntityViewPage> {
String _title; String _title;
Entity _entity;
StreamSubscription _stateSubscription; StreamSubscription _stateSubscription;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_entity = widget.entity;
if (_stateSubscription != null) _stateSubscription.cancel(); if (_stateSubscription != null) _stateSubscription.cancel();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.entityId == _entity.entityId) { if (event.entityId == widget.entity.entityId) {
setState(() {}); setState(() {});
} }
}); });
_prepareData(); _prepareData();
_getHistory();
} }
_prepareData() async { void _prepareData() async {
_title = _entity.displayName; _title = widget.entity.displayName;
} }
void _getHistory() {
/* widget.homeAssistant.getHistory(widget.entity.entityId).then((List history) {
if (history != null) {
}
});*/
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Scaffold( return new Scaffold(
@ -44,22 +53,13 @@ class _EntityViewPageState extends State<EntityViewPage> {
), ),
body: Padding( body: Padding(
padding: EdgeInsets.all(10.0), padding: EdgeInsets.all(10.0),
child: ListView( child: widget.entity.buildEntityPageWidget(context)
children: <Widget>[
_entity.buildWidget(false, context),
_entity.buildAdditionalWidget()
],
),
), ),
); );
} }
@override @override
void dispose(){ void dispose(){
if (_entity is TextEntity && (_entity as TextEntity).tmpState != _entity.state) {
eventBus.fire(new ServiceCallEvent(_entity.domain, "set_value", _entity.entityId, {"value": "${(_entity as TextEntity).tmpState}"}));
TheLogger.log("Debug", "Saving changed input value for ${_entity.entityId}");
}
if (_stateSubscription != null) _stateSubscription.cancel(); if (_stateSubscription != null) _stateSubscription.cancel();
super.dispose(); super.dispose();
} }

View File

@ -0,0 +1,466 @@
part of '../main.dart';
class Entity {
static const STATE_ICONS_COLORS = {
"on": Colors.amber,
"off": Color.fromRGBO(68, 115, 158, 1.0),
"default": Color.fromRGBO(68, 115, 158, 1.0),
"unavailable": Colors.black12,
"unknown": Colors.black12,
"playing": Colors.amber
};
static const badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
};
static List badgeDomains = [
"alarm_control_panel",
"binary_sensor",
"device_tracker",
"updater",
"sun",
"timer",
"sensor"
];
double rightWidgetPadding = 14.0;
double leftWidgetPadding = 8.0;
double extendedWidgetHeight = 50.0;
double widgetHeight = 34.0;
double iconSize = 28.0;
double stateFontSize = 16.0;
double nameFontSize = 16.0;
double smallFontSize = 14.0;
double largeFontSize = 24.0;
double inputWidth = 160.0;
double rowPadding = 10.0;
Map attributes;
String domain;
String entityId;
String state;
String assumedState;
DateTime _lastUpdated;
List<Entity> childEntities = [];
List<String> attributesToShow = ["all"];
String get displayName =>
attributes["friendly_name"] ?? (attributes["name"] ?? "_");
String get deviceClass => attributes["device_class"] ?? null;
bool get isView =>
(domain == "group") &&
(attributes != null ? attributes["view"] ?? false : false);
bool get isGroup => domain == "group";
bool get isBadge => Entity.badgeDomains.contains(domain);
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 childEntityIds => 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"];
assumedState = state;
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
}
double _getDoubleAttributeValue(String attributeName) {
var temp1 = attributes["$attributeName"];
if (temp1 is int) {
return temp1.toDouble();
} else if (temp1 is double) {
return temp1;
} else {
return null;
}
}
Widget buildDefaultWidget(BuildContext context) {
return EntityModel(
entity: this,
child: DefaultEntityContainer(state: _buildStatePart(context)),
handleTap: true,
);
}
Widget _buildStatePart(BuildContext context) {
return SimpleEntityState();
}
Widget _buildStatePartForPage(BuildContext context) {
return _buildStatePart(context);
}
Widget _buildAdditionalControlsForPage(BuildContext context) {
return Container(
width: 0.0,
height: 0.0,
);
}
Widget buildEntityPageWidget(BuildContext context) {
return EntityModel(
entity: this,
child: EntityPageContainer(children: <Widget>[
DefaultEntityContainer(state: _buildStatePartForPage(context)),
LastUpdatedWidget(),
Divider(),
_buildAdditionalControlsForPage(context),
Divider(),
EntityAttributesList()
]),
handleTap: false,
);
}
Widget buildBadgeWidget(BuildContext context) {
return EntityModel(
entity: this,
child: Badge(),
handleTap: true,
);
}
String getAttribute(String attributeName) {
if (attributes != null) {
return attributes["$attributeName"];
}
return null;
}
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 SwitchEntity extends Entity {
SwitchEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return SwitchControlWidget();
}
}
class ButtonEntity extends Entity {
ButtonEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return ButtonControlWidget();
}
}
class TextEntity extends Entity {
TextEntity(Map rawData) : super(rawData);
int get valueMinLength => attributes["min"] ?? -1;
int get valueMaxLength => attributes["max"] ?? -1;
String get valuePattern => attributes["pattern"] ?? null;
bool get isTextField => attributes["mode"] == "text";
bool get isPasswordField => attributes["mode"] == "password";
@override
Widget _buildStatePart(BuildContext context) {
return TextControlWidget();
}
}
class SunEntity extends Entity {
SunEntity(Map rawData) : super(rawData);
}
class SliderEntity extends Entity {
SliderEntity(Map rawData) : super(rawData);
double get minValue => attributes["min"] ?? 0.0;
double get maxValue => attributes["max"] ?? 100.0;
double get valueStep => attributes["step"] ?? 1.0;
double get doubleState => double.tryParse(state) ?? 0.0;
@override
Widget _buildStatePart(BuildContext context) {
return Expanded(
//width: 200.0,
child: Row(
children: <Widget>[
SliderControlWidget(
expanded: true,
),
SimpleEntityState(),
],
),
);
}
@override
Widget _buildStatePartForPage(BuildContext context) {
return SimpleEntityState();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return SliderControlWidget(
expanded: false,
);
}
}
class ClimateEntity extends Entity {
@override
double widgetHeight = 38.0;
static const SUPPORT_TARGET_TEMPERATURE = 1;
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
static const SUPPORT_TARGET_HUMIDITY = 8;
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
static const SUPPORT_FAN_MODE = 64;
static const SUPPORT_OPERATION_MODE = 128;
static const SUPPORT_HOLD_MODE = 256;
static const SUPPORT_SWING_MODE = 512;
static const SUPPORT_AWAY_MODE = 1024;
static const SUPPORT_AUX_HEAT = 2048;
static const SUPPORT_ON_OFF = 4096;
bool get supportTargetTemperature => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
bool get supportTargetTemperatureHigh => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
bool get supportTargetTemperatureLow => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
bool get supportTargetHumidity => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
bool get supportTargetHumidityHigh => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
bool get supportTargetHumidityLow => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
bool get supportFanMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_FAN_MODE) ==
ClimateEntity.SUPPORT_FAN_MODE);
bool get supportOperationMode => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_OPERATION_MODE) ==
ClimateEntity.SUPPORT_OPERATION_MODE);
bool get supportHoldMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_HOLD_MODE) ==
ClimateEntity.SUPPORT_HOLD_MODE);
bool get supportSwingMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_SWING_MODE) ==
ClimateEntity.SUPPORT_SWING_MODE);
bool get supportAwayMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_AWAY_MODE) ==
ClimateEntity.SUPPORT_AWAY_MODE);
bool get supportAuxHeat =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_AUX_HEAT) ==
ClimateEntity.SUPPORT_AUX_HEAT);
bool get supportOnOff =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_ON_OFF) ==
ClimateEntity.SUPPORT_ON_OFF);
List<String> get operationList => attributes["operation_list"] != null
? (attributes["operation_list"] as List).cast<String>()
: null;
List<String> get fanList => attributes["fan_list"] != null
? (attributes["fan_list"] as List).cast<String>()
: null;
List<String> get swingList => attributes["swing_list"] != null
? (attributes["swing_list"] as List).cast<String>()
: null;
double get temperature => _getDoubleAttributeValue('temperature');
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
double get targetLow => _getDoubleAttributeValue('target_temp_low');
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
double get minTemp => _getDoubleAttributeValue('min_temp') ?? -100.0;
double get targetHumidity => _getDoubleAttributeValue('humidity');
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
double get minHumidity => _getDoubleAttributeValue('min_humidity');
String get operationMode => attributes['operation_mode'];
String get fanMode => attributes['fan_mode'];
String get swingMode => attributes['swing_mode'];
bool get awayMode => attributes['away_mode'] == "on";
bool get isOff => state == "off";
bool get auxHeat => attributes['aux_heat'] == "on";
ClimateEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return ClimateStateWidget();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return ClimateControlWidget();
}
@override
double _getDoubleAttributeValue(String attributeName) {
var temp1 = attributes["$attributeName"];
if (temp1 is int) {
return temp1.toDouble();
} else if (temp1 is double) {
return temp1;
} else {
return null;
}
}
}
class SelectEntity extends Entity {
List<String> get listOptions => attributes["options"] != null
? (attributes["options"] as List).cast<String>()
: [];
SelectEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return SelectControlWidget();
}
}
class DateTimeEntity extends Entity {
bool get hasDate => attributes["has_date"] ?? false;
bool get hasTime => attributes["has_time"] ?? false;
int get year => attributes["year"] ?? 1970;
int get month => attributes["month"] ?? 1;
int get day => attributes["day"] ?? 1;
int get hour => attributes["hour"] ?? 0;
int get minute => attributes["minute"] ?? 0;
int get second => attributes["second"] ?? 0;
String get formattedState => _getFormattedState();
DateTime get dateTimeState => _getDateTimeState();
DateTimeEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return DateTimeStateWidget();
}
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;
}
void setNewState(newValue) {
eventBus
.fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue));
}
}
class CoverEntity extends Entity {
@override
double widgetHeight = 38.0;
static const SUPPORT_OPEN = 1;
static const SUPPORT_CLOSE = 2;
static const SUPPORT_SET_POSITION = 4;
static const SUPPORT_STOP = 8;
static const SUPPORT_OPEN_TILT = 16;
static const SUPPORT_CLOSE_TILT = 32;
static const SUPPORT_STOP_TILT = 64;
static const SUPPORT_SET_TILT_POSITION = 128;
bool get supportOpen => ((attributes["supported_features"] &
CoverEntity.SUPPORT_OPEN) ==
CoverEntity.SUPPORT_OPEN);
bool get supportClose => ((attributes["supported_features"] &
CoverEntity.SUPPORT_CLOSE) ==
CoverEntity.SUPPORT_CLOSE);
bool get supportSetPosition => ((attributes["supported_features"] &
CoverEntity.SUPPORT_SET_POSITION) ==
CoverEntity.SUPPORT_SET_POSITION);
bool get supportStop => ((attributes["supported_features"] &
CoverEntity.SUPPORT_STOP) ==
CoverEntity.SUPPORT_STOP);
bool get supportOpenTilt => ((attributes["supported_features"] &
CoverEntity.SUPPORT_OPEN_TILT) ==
CoverEntity.SUPPORT_OPEN_TILT);
bool get supportCloseTilt => ((attributes["supported_features"] &
CoverEntity.SUPPORT_CLOSE_TILT) ==
CoverEntity.SUPPORT_CLOSE_TILT);
bool get supportStopTilt => ((attributes["supported_features"] &
CoverEntity.SUPPORT_STOP_TILT) ==
CoverEntity.SUPPORT_STOP_TILT);
bool get supportSetTiltPosition => ((attributes["supported_features"] &
CoverEntity.SUPPORT_SET_TILT_POSITION) ==
CoverEntity.SUPPORT_SET_TILT_POSITION);
double get currentPosition => _getDoubleAttributeValue('current_position');
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position');
bool get canBeOpened => ((state == "closed") || (state == "closing") || (state == "opening"));
bool get canBeClosed => ((state == "open") || (state == "opening")|| (state == "closing"));
bool get canTiltBeOpened => currentPosition < 100;
bool get canTiltBeClosed => currentPosition > 0;
CoverEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return CoverEntityControlState();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return CoverControlWidget();
}
}

View File

@ -0,0 +1,954 @@
part of '../main.dart';
class SwitchControlWidget extends StatefulWidget {
@override
_SwitchControlWidgetState createState() => _SwitchControlWidgetState();
}
class _SwitchControlWidgetState extends State<SwitchControlWidget> {
@override
void initState() {
super.initState();
}
void _setNewState(newValue, Entity entity) {
setState(() {
entity.assumedState = newValue ? 'on' : 'off';
});
Timer(Duration(seconds: 2), (){
setState(() {
entity.assumedState = entity.state;
});
});
eventBus.fire(new ServiceCallEvent(
entity.domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return Switch(
value: entityModel.entity.assumedState == 'on',
onChanged: ((switchState) {
_setNewState(switchState, entityModel.entity);
}),
);
}
}
class ButtonControlWidget extends StatefulWidget {
@override
_ButtonControlWidgetState createState() => _ButtonControlWidgetState();
}
class _ButtonControlWidgetState extends State<ButtonControlWidget> {
@override
void initState() {
super.initState();
}
void _setNewState(Entity entity) {
eventBus.fire(new ServiceCallEvent(entity.domain, "turn_on", entity.entityId, null));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return FlatButton(
onPressed: (() {
_setNewState(entityModel.entity);
}),
child: Text(
"EXECUTE",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: entityModel.entity.stateFontSize, color: Colors.blue),
),
);
}
}
class TextControlWidget extends StatefulWidget {
TextControlWidget({Key key}) : super(key: key);
@override
_TextControlWidgetState createState() => _TextControlWidgetState();
}
class _TextControlWidgetState extends State<TextControlWidget> {
String _tmpValue;
String _entityState;
String _entityDomain;
String _entityId;
int _minLength;
int _maxLength;
FocusNode _focusNode = FocusNode();
bool validValue = false;
@override
void initState() {
super.initState();
_focusNode.addListener(_focusListener);
}
void setNewState(newValue, domain, entityId) {
if (validate(newValue, _minLength, _maxLength)) {
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
{"value": "$newValue"}));
} else {
setState(() {
_tmpValue = _entityState;
});
}
}
bool validate(newValue, minLength, maxLength) {
if (newValue is String) {
validValue = (newValue.length >= minLength) &&
(maxLength == -1 ||
(newValue.length <= maxLength));
} else {
validValue = true;
}
return validValue;
}
void _focusListener() {
if (!_focusNode.hasFocus && (_tmpValue != _entityState)) {
setNewState(_tmpValue, _entityDomain, _entityId);
}
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final TextEntity entity = entityModel.entity;
_entityState = entity.state;
_entityDomain = entity.domain;
_entityId = entity.entityId;
_minLength = entity.valueMinLength;
_maxLength = entity.valueMaxLength;
if (!_focusNode.hasFocus && (_tmpValue != entity.state)) {
_tmpValue = entity.state;
}
if (entity.isTextField || entity.isPasswordField) {
return Expanded(
//width: Entity.INPUT_WIDTH,
child: TextField(
focusNode: _focusNode,
obscureText: entity.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 ${entity.entityId}");
return SimpleEntityState();
}
}
@override
void dispose() {
_focusNode.removeListener(_focusListener);
_focusNode.dispose();
super.dispose();
}
}
class SliderControlWidget extends StatefulWidget {
final bool expanded;
SliderControlWidget({Key key, @required this.expanded}) : super(key: key);
@override
_SliderControlWidgetState createState() => _SliderControlWidgetState();
}
class _SliderControlWidgetState extends State<SliderControlWidget> {
int _multiplier = 1;
void setNewState(newValue, domain, entityId) {
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
{"value": "${newValue.toString()}"}));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final SliderEntity entity = entityModel.entity;
if (entity.valueStep < 1) {
_multiplier = 10;
} else if (entity.valueStep < 0.1) {
_multiplier = 100;
}
Widget slider = Slider(
min: entity.minValue * _multiplier,
max: entity.maxValue * _multiplier,
value: (entity.doubleState <= entity.maxValue) &&
(entity.doubleState >= entity.minValue)
? entity.doubleState * _multiplier
: entity.minValue * _multiplier,
onChanged: (value) {
setState(() {
entity.state =
(value.roundToDouble() / _multiplier).toString();
});
eventBus.fire(new StateChangedEvent(entity.entityId,
(value.roundToDouble() / _multiplier).toString(), true));
},
onChangeEnd: (value) {
setNewState(value.roundToDouble() / _multiplier, entity.domain, entity.entityId);
},
);
if (widget.expanded) {
return Expanded(
child: slider,
);
} else {
return slider;
}
}
}
class ClimateControlWidget extends StatefulWidget {
ClimateControlWidget({Key key}) : super(key: key);
@override
_ClimateControlWidgetState createState() => _ClimateControlWidgetState();
}
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
bool _showPending = false;
bool _changedHere = false;
Timer _resetTimer;
double _tmpTemperature = 0.0;
double _tmpTargetLow = 0.0;
double _tmpTargetHigh = 0.0;
double _tmpTargetHumidity = 0.0;
String _tmpOperationMode;
String _tmpFanMode;
String _tmpSwingMode;
bool _tmpAwayMode = false;
bool _tmpIsOff = false;
bool _tmpAuxHeat = false;
void _resetVars(ClimateEntity entity) {
_tmpTemperature = entity.temperature;
_tmpTargetHigh = entity.targetHigh;
_tmpTargetLow = entity.targetLow;
_tmpOperationMode = entity.operationMode;
_tmpFanMode = entity.fanMode;
_tmpSwingMode = entity.swingMode;
_tmpAwayMode = entity.awayMode;
_tmpIsOff = entity.isOff;
_tmpAuxHeat = entity.auxHeat;
_tmpTargetHumidity = entity.targetHumidity;
_showPending = false;
_changedHere = false;
}
void _temperatureUp(ClimateEntity entity, double step) {
_tmpTemperature = ((_tmpTemperature + step) <= entity.maxTemp) ? _tmpTemperature + step : entity.maxTemp;
_setTemperature(entity);
}
void _temperatureDown(ClimateEntity entity, double step) {
_tmpTemperature = ((_tmpTemperature - step) >= entity.minTemp) ? _tmpTemperature - step : entity.minTemp;
_setTemperature(entity);
}
void _targetLowUp(ClimateEntity entity, double step) {
_tmpTargetLow = ((_tmpTargetLow + step) <= entity.maxTemp) ? _tmpTargetLow + step : entity.maxTemp;
_setTargetTemp(entity);
}
void _targetLowDown(ClimateEntity entity, double step) {
_tmpTargetLow = ((_tmpTargetLow - step) >= entity.minTemp) ? _tmpTargetLow - step : entity.minTemp;
_setTargetTemp(entity);
}
void _targetHighUp(ClimateEntity entity, double step) {
_tmpTargetHigh = ((_tmpTargetHigh + step) <= entity.maxTemp) ? _tmpTargetHigh + step : entity.maxTemp;
_setTargetTemp(entity);
}
void _targetHighDown(ClimateEntity entity, double step) {
_tmpTargetHigh = ((_tmpTargetHigh - step) >= entity.minTemp) ? _tmpTargetHigh - step : entity.minTemp;
_setTargetTemp(entity);
}
void _setTemperature(ClimateEntity entity) {
setState(() {
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
_resetStateTimer(entity);
});
}
void _setTargetTemp(ClimateEntity entity) {
setState(() {
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}));
_resetStateTimer(entity);
});
}
void _setTargetHumidity(ClimateEntity entity, double value) {
setState(() {
_tmpTargetHumidity = value.roundToDouble();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_humidity", entity.entityId,{"humidity": "$_tmpTargetHumidity"}));
_resetStateTimer(entity);
});
}
void _setOperationMode(ClimateEntity entity, value) {
setState(() {
_tmpOperationMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_operation_mode", entity.entityId,{"operation_mode": "$_tmpOperationMode"}));
_resetStateTimer(entity);
});
}
void _setSwingMode(ClimateEntity entity, value) {
setState(() {
_tmpSwingMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_swing_mode", entity.entityId,{"swing_mode": "$_tmpSwingMode"}));
_resetStateTimer(entity);
});
}
void _setFanMode(ClimateEntity entity, value) {
setState(() {
_tmpFanMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_fan_mode", entity.entityId,{"fan_mode": "$_tmpFanMode"}));
_resetStateTimer(entity);
});
}
void _setAwayMode(ClimateEntity entity, value) {
setState(() {
_tmpAwayMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_away_mode", entity.entityId,{"away_mode": "${_tmpAwayMode ? 'on' : 'off'}"}));
_resetStateTimer(entity);
});
}
void _setOnOf(ClimateEntity entity, value) {
setState(() {
_tmpIsOff = !value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
_resetStateTimer(entity);
});
}
void _setAuxHeat(ClimateEntity entity, value) {
setState(() {
_tmpAuxHeat = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"}));
_resetStateTimer(entity);
});
}
void _resetStateTimer(ClimateEntity entity) {
if (_resetTimer!=null) {
_resetTimer.cancel();
}
_resetTimer = Timer(Duration(seconds: 3), () {
setState(() {});
_resetVars(entity);
});
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entity;
if (_changedHere) {
_showPending = (_tmpTemperature != entity.temperature);
_changedHere = false;
} else {
_resetTimer?.cancel();
_resetVars(entity);
}
return Padding(
padding: EdgeInsets.fromLTRB(entity.leftWidgetPadding, entity.rowPadding, entity.rightWidgetPadding, 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildOnOffControl(entity),
_buildTemperatureControls(entity),
_buildHumidityControls(entity),
_buildOperationControl(entity),
_buildFanControl(entity),
_buildSwingControl(entity),
_buildAwayModeControl(entity),
_buildAuxHeatControl(entity)
],
),
);
}
Widget _buildAwayModeControl(ClimateEntity entity) {
if (entity.supportAwayMode) {
return Row(
children: <Widget>[
Expanded(
child: Text(
"Away mode",
style: TextStyle(
fontSize: entity.stateFontSize
),
),
),
Switch(
onChanged: (value) => _setAwayMode(entity, value),
value: _tmpAwayMode,
)
],
);
} else {
return Container(height: 0.0, width: 0.0,);
}
}
Widget _buildOnOffControl(ClimateEntity entity) {
if (entity.supportOnOff) {
return Row(
children: <Widget>[
Expanded(
child: Text(
"On / Off",
style: TextStyle(
fontSize: entity.stateFontSize
),
),
),
Switch(
onChanged: (value) => _setOnOf(entity, value),
value: !_tmpIsOff,
)
],
);
} else {
return Container(height: 0.0, width: 0.0,);
}
}
Widget _buildAuxHeatControl(ClimateEntity entity) {
if (entity.supportAuxHeat ) {
return Row(
children: <Widget>[
Expanded(
child: Text(
"Aux heat",
style: TextStyle(
fontSize: entity.stateFontSize
),
),
),
Switch(
onChanged: (value) => _setAuxHeat(entity, value),
value: _tmpAuxHeat,
)
],
);
} else {
return Container(height: 0.0, width: 0.0,);
}
}
Widget _buildOperationControl(ClimateEntity entity) {
if (entity.supportOperationMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Operation", style: TextStyle(
fontSize: entity.stateFontSize
)),
DropdownButton<String>(
value: "$_tmpOperationMode",
iconSize: 30.0,
style: TextStyle(
fontSize: entity.largeFontSize,
color: Colors.black,
),
items: entity.operationList.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (mode) => _setOperationMode(entity, mode),
),
Container(height: entity.rowPadding,)
],
);
} else {
return Container(height: 0.0, width: 0.0);
}
}
Widget _buildFanControl(ClimateEntity entity) {
if (entity.supportFanMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Fan mode", style: TextStyle(
fontSize: entity.stateFontSize
)),
DropdownButton<String>(
value: "$_tmpFanMode",
iconSize: 30.0,
style: TextStyle(
fontSize: entity.largeFontSize,
color: Colors.black,
),
items: entity.fanList.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (mode) => _setFanMode(entity, mode),
),
Container(height: entity.rowPadding,)
],
);
} else {
return Container(height: 0.0, width: 0.0);
}
}
Widget _buildSwingControl(ClimateEntity entity) {
if (entity.supportSwingMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Swing mode", style: TextStyle(
fontSize: entity.stateFontSize
)),
DropdownButton<String>(
value: "$_tmpSwingMode",
iconSize: 30.0,
style: TextStyle(
fontSize: entity.largeFontSize,
color: Colors.black,
),
items: entity.swingList.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (mode) => _setSwingMode(entity, mode),
),
Container(height: entity.rowPadding,)
],
);
} else {
return Container(height: 0.0, width: 0.0);
}
}
Widget _buildTemperatureControls(ClimateEntity entity) {
List<Widget> result = [];
if (entity.supportTargetTemperature) {
result.addAll(<Widget>[
Text(
"$_tmpTemperature",
style: TextStyle(
fontSize: entity.largeFontSize,
color: _showPending ? Colors.red : Colors.black
),
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-up')),
iconSize: 30.0,
onPressed: () => _temperatureUp(entity, 0.1),
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-down')),
iconSize: 30.0,
onPressed: () => _temperatureDown(entity, 0.1),
)
],
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-up')),
iconSize: 30.0,
onPressed: () => _temperatureUp(entity, 0.5),
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-down')),
iconSize: 30.0,
onPressed: () => _temperatureDown(entity, 0.5),
)
],
)
]);
} else if (entity.supportTargetTemperatureHigh && entity.supportTargetTemperatureLow) {
result.addAll(<Widget>[
Text(
"$_tmpTargetLow",
style: TextStyle(
fontSize: entity.largeFontSize,
color: _showPending ? Colors.red : Colors.black
),
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-up')),
iconSize: 30.0,
onPressed: () => _targetLowUp(entity, 0.1),
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-down')),
iconSize: 30.0,
onPressed: () => _targetLowDown(entity, 0.1),
)
],
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-up')),
iconSize: 30.0,
onPressed: () => _targetLowUp(entity, 0.5),
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-down')),
iconSize: 30.0,
onPressed: () => _targetLowDown(entity, 0.5),
)
],
),
Expanded(
child: Container(height: 10.0),
),
Text(
"$_tmpTargetHigh",
style: TextStyle(
fontSize: entity.largeFontSize,
color: _showPending ? Colors.red : Colors.black
),
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-up')),
iconSize: 30.0,
onPressed: () => _targetHighUp(entity, 0.1),
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-down')),
iconSize: 30.0,
onPressed: () => _targetHighDown(entity, 0.1),
)
],
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-up')),
iconSize: 30.0,
onPressed: () => _targetHighUp(entity, 0.5),
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-down')),
iconSize: 30.0,
onPressed: () => _targetHighDown(entity, 0.5),
)
],
)
]);
} else if (entity.supportTargetTemperatureHigh || entity.supportTargetTemperatureLow) {
result.add(Text("Unsupported temperature control. Please, report an issue."));
}
if (result.isNotEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Target temperature", style: TextStyle(
fontSize: entity.stateFontSize
)),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: result,
)
],
);
} else {
return Container(height: 0.0, width: 0.0,);
}
}
Widget _buildHumidityControls(ClimateEntity entity) {
List<Widget> result = [];
if (entity.supportTargetHumidity) {
result.addAll(<Widget>[
Text(
"$_tmpTargetHumidity%",
style: TextStyle(fontSize: entity.largeFontSize),
),
Expanded(
child: Slider(
value: _tmpTargetHumidity,
max: entity.maxHumidity,
min: entity.minHumidity,
onChanged: ((double val) {
setState(() {
_changedHere = true;
_tmpTargetHumidity = val.roundToDouble();
});
}),
onChangeEnd: (double v) => _setTargetHumidity(entity, v),
),
)
]);
}
if (result.isNotEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(
0.0, entity.rowPadding, 0.0, entity.rowPadding),
child: Text("Target humidity", style: TextStyle(
fontSize: entity.stateFontSize
)),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: result,
),
Container(
height: entity.rowPadding,
)
],
);
} else {
return Container(
width: 0.0,
height: 0.0,
);
}
}
@override
void dispose() {
_resetTimer?.cancel();
super.dispose();
}
}
class SelectControlWidget extends StatefulWidget {
SelectControlWidget({Key key}) : super(key: key);
@override
_SelectControlWidgetState createState() => _SelectControlWidgetState();
}
class _SelectControlWidgetState extends State<SelectControlWidget> {
void setNewState(domain, entityId, newValue) {
eventBus.fire(new ServiceCallEvent(domain, "select_option", entityId,
{"option": "$newValue"}));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final SelectEntity entity = entityModel.entity;
Widget ctrl;
if (entity.listOptions.isNotEmpty) {
ctrl = DropdownButton<String>(
value: entity.state,
items: entity.listOptions.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (_) {
setNewState(entity.domain, entity.entityId,_);
},
);
} else {
ctrl = Text('---');
}
return Expanded(
//width: Entity.INPUT_WIDTH,
child: ctrl,
);
}
}
class CoverControlWidget extends StatefulWidget {
CoverControlWidget({Key key}) : super(key: key);
@override
_CoverControlWidgetState createState() => _CoverControlWidgetState();
}
class _CoverControlWidgetState extends State<CoverControlWidget> {
double _tmpPosition = 0.0;
double _tmpTiltPosition = 0.0;
bool _changedHere = false;
void _setNewPosition(CoverEntity entity, double position) {
setState(() {
_tmpPosition = position.roundToDouble();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()}));
});
}
void _setNewTiltPosition(CoverEntity entity, double position) {
setState(() {
_tmpTiltPosition = position.roundToDouble();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()}));
});
}
void _resetVars(CoverEntity entity) {
_tmpPosition = entity.currentPosition;
_tmpTiltPosition = entity.currentTiltPosition;
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final CoverEntity entity = entityModel.entity;
if (_changedHere) {
_changedHere = false;
} else {
_resetVars(entity);
}
return Padding(
padding: EdgeInsets.fromLTRB(entity.leftWidgetPadding, entity.rowPadding, entity.rightWidgetPadding, 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildPositionControls(entity),
_buildTiltControls(entity)
],
),
);
}
Widget _buildPositionControls(CoverEntity entity) {
if (entity.supportSetPosition) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(
0.0, entity.rowPadding, 0.0, entity.rowPadding),
child: Text("Position", style: TextStyle(
fontSize: entity.stateFontSize
)),
),
Slider(
value: _tmpPosition,
min: 0.0,
max: 100.0,
divisions: 10,
onChanged: (double value) {
setState(() {
_tmpPosition = value.roundToDouble();
_changedHere = true;
});
},
onChangeEnd: (double value) => _setNewPosition(entity, value),
),
Container(height: entity.rowPadding,)
],
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildTiltControls(CoverEntity entity) {
List<Widget> controls = [];
if (entity.supportCloseTilt || entity.supportOpenTilt || entity.supportStopTilt) {
controls.add(
CoverEntityTiltControlState()
);
}
if (entity.supportSetTiltPosition) {
controls.addAll(<Widget>[
Slider(
value: _tmpTiltPosition,
min: 0.0,
max: 100.0,
divisions: 10,
onChanged: (double value) {
setState(() {
_tmpTiltPosition = value.roundToDouble();
_changedHere = true;
});
},
onChangeEnd: (double value) => _setNewTiltPosition(entity, value),
),
Container(height: entity.rowPadding,)
]);
}
if (controls.isNotEmpty) {
controls.insert(0, Padding(
padding: EdgeInsets.fromLTRB(
0.0, entity.rowPadding, 0.0, entity.rowPadding),
child: Text("Tilt position", style: TextStyle(
fontSize: entity.stateFontSize
)),
));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: controls,
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
}

View File

@ -0,0 +1,569 @@
part of '../main.dart';
class EntityWidgetsSizes {
}
class EntityModel extends InheritedWidget {
const EntityModel({
Key key,
@required this.entity,
@required this.handleTap,
@required Widget child,
}) : super(key: key, child: child);
final Entity entity;
final bool handleTap;
static EntityModel of(BuildContext context) {
return context.inheritFromWidgetOfExactType(EntityModel);
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
}
}
class DefaultEntityContainer extends StatelessWidget {
DefaultEntityContainer({
Key key,
@required this.state,
}) : super(key: key);
final Widget state;
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return SizedBox(
height: entityModel.entity.widgetHeight,
child: Row(
children: <Widget>[
EntityIcon(),
Expanded(
child: EntityName(),
),
state
],
),
);
}
}
class EntityPageContainer extends StatelessWidget {
EntityPageContainer({Key key, @required this.children}) : super(key: key);
final List<Widget> children;
@override
Widget build(BuildContext context) {
return ListView(
children: children,
);
}
}
class SimpleEntityState extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return Padding(
padding:
EdgeInsets.fromLTRB(0.0, 0.0, entityModel.entity.rightWidgetPadding, 0.0),
child: GestureDetector(
child: Text(
"${entityModel.entity.state}${entityModel.entity.unitOfMeasurement}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: entityModel.entity.stateFontSize,
)),
onTap: () => entityModel.handleTap ? eventBus.fire(new ShowEntityPageEvent(entityModel.entity)) : null,
)
);
}
}
class EntityName extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return GestureDetector(
child: Padding(
padding: EdgeInsets.only(right: 10.0),
child: Text(
"${entityModel.entity.displayName}",
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle(fontSize: entityModel.entity.nameFontSize),
),
),
onTap: () => entityModel.handleTap ? eventBus.fire(new ShowEntityPageEvent(entityModel.entity)) : null,
);
}
}
class EntityIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return GestureDetector(
child: Padding(
padding: EdgeInsets.fromLTRB(entityModel.entity.leftWidgetPadding, 0.0, 12.0, 0.0),
//TODO: move createIconWidgetFromEntityData into this widget
child: MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entity,
entityModel.entity.iconSize,
Entity.STATE_ICONS_COLORS[entityModel.entity.state] ?? Entity.STATE_ICONS_COLORS["default"]),
),
onTap: () => entityModel.handleTap ? eventBus.fire(new ShowEntityPageEvent(entityModel.entity)) : null,
);
}
}
class LastUpdatedWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return Padding(
padding: EdgeInsets.fromLTRB(
entityModel.entity.leftWidgetPadding, 0.0, 0.0, 0.0),
child: Text(
'${entityModel.entity.lastUpdated}',
textAlign: TextAlign.left,
style:
TextStyle(fontSize: entityModel.entity.smallFontSize, color: Colors.black26),
),
);
}
}
class EntityAttributesList extends StatelessWidget {
EntityAttributesList({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
List<Widget> attrs = [];
if ((entityModel.entity.attributesToShow == null) || (entityModel.entity.attributesToShow.contains("all"))) {
entityModel.entity.attributes.forEach((name, value){
attrs.add(
_buildSingleAttribute(entityModel.entity, "$name", "$value")
);
});
} else {
entityModel.entity.attributesToShow.forEach((String attr) {
String attrValue = entityModel.entity.getAttribute("$attr");
if (attrValue != null) {
attrs.add(
_buildSingleAttribute(entityModel.entity, "$attr", "$attrValue")
);
}
});
}
return Column(
children: attrs,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
);
}
Widget _buildSingleAttribute(Entity entity, String name, String value) {
return Row(
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(entity.leftWidgetPadding, entity.rowPadding, 0.0, 0.0),
child: Text(
"$name",
textAlign: TextAlign.left,
),
),
),
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(0.0, entity.rowPadding, entity.rightWidgetPadding, 0.0),
child: Text(
"$value",
textAlign: TextAlign.right,
),
),
)
],
);
}
}
class Badge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
double iconSize = 26.0;
Widget badgeIcon;
String onBadgeTextValue;
Color iconColor = Entity.badgeColors[entityModel.entity.domain] ?? Entity.badgeColors["default"];
switch (entityModel.entity.domain) {
case "sun": {
badgeIcon = entityModel.entity.state == "below_horizon" ?
Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
size: iconSize,
) :
Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8),
size: iconSize,
);
break;
}
case "sensor": {
onBadgeTextValue = entityModel.entity.unitOfMeasurement;
badgeIcon = Center(
child: Text(
"${entityModel.entity.state}",
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 17.0),
),
);
break;
}
case "device_tracker": {
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(entityModel.entity, iconSize,Colors.black);
onBadgeTextValue = entityModel.entity.state;
break;
}
default: {
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(entityModel.entity, iconSize,Colors.black);
}
}
Widget onBadgeText;
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
onBadgeText = Container(width: 0.0, height: 0.0);
} else {
onBadgeText = Container(
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
child: Text("$onBadgeTextValue",
style: TextStyle(fontSize: 12.0, color: Colors.white),
textAlign: TextAlign.center, softWrap: false, overflow: TextOverflow.fade),
decoration: new BoxDecoration(
// Circle shape
//shape: BoxShape.circle,
color: iconColor,
borderRadius: BorderRadius.circular(9.0),
)
);
}
return GestureDetector(
child: Column(
children: <Widget>[
Container(
margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
width: 50.0,
height: 50.0,
decoration: new BoxDecoration(
// Circle shape
shape: BoxShape.circle,
color: Colors.white,
// The border you want
border: new Border.all(
width: 2.0,
color: iconColor,
),
),
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
width: 46.0,
height: 46.0,
top: 0.0,
left: 0.0,
child: badgeIcon,
),
Positioned(
//width: 50.0,
bottom: -9.0,
left: -10.0,
right: -10.0,
child: Center(
child: onBadgeText,
)
)
],
),
),
Container(
width: 60.0,
child: Text(
"${entityModel.entity.displayName}",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12.0),
softWrap: true,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
onTap: () => eventBus.fire(new ShowEntityPageEvent(entityModel.entity))
);
}
}
class ClimateStateWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entity;
return Padding(
padding:
EdgeInsets.fromLTRB(0.0, 0.0, entityModel.entity.rightWidgetPadding, 0.0),
child: GestureDetector(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Row(
children: <Widget>[
Text(
"${entity.state}",
textAlign: TextAlign.right,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: entityModel.entity.stateFontSize,
)),
Text(
entity.supportTargetTemperature ? " ${entity.temperature}" : " ${entity.targetLow} - ${entity.targetHigh}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: entityModel.entity.stateFontSize,
))
],
),
Text(
"Currently: ${entity.attributes["current_temperature"]}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: entityModel.entity.stateFontSize,
color: Colors.black45
))
],
),
onTap: () => entityModel.handleTap ? eventBus.fire(new ShowEntityPageEvent(entity)) : null,
)
);
}
}
class DateTimeStateWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final DateTimeEntity entity = entityModel.entity;
return Padding(
padding:
EdgeInsets.fromLTRB(0.0, 0.0, entity.rightWidgetPadding, 0.0),
child: GestureDetector(
child: Text(
"${entity.formattedState}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: entity.stateFontSize,
)),
onTap: () => _handleStateTap(context, entity),
)
);
}
void _handleStateTap(BuildContext context, DateTimeEntity entity) {
if (entity.hasDate) {
_showDatePicker(context, entity).then((date) {
if (date != null) {
if (entity.hasTime) {
_showTimePicker(context, entity).then((time){
entity.setNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}", "time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
});
} else {
entity.setNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}"});
}
}
});
} else if (entity.hasTime) {
_showTimePicker(context, entity).then((time){
if (time != null) {
entity.setNewState({"time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
}
});
} else {
TheLogger.log("Warning", "${entity.entityId} has no date and no time");
}
}
Future _showDatePicker(BuildContext context, DateTimeEntity entity) {
return showDatePicker(
context: context,
initialDate: entity.dateTimeState,
firstDate: DateTime(1970),
lastDate: DateTime(2037) //Unix timestamp will finish at Jan 19, 2038
);
}
Future _showTimePicker(BuildContext context, DateTimeEntity entity) {
return showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(entity.dateTimeState)
);
}
}
class CoverEntityControlState extends StatelessWidget {
void _open(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(entity.domain, "open_cover", entity.entityId, null));
}
void _close(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(entity.domain, "close_cover", entity.entityId, null));
}
void _stop(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(entity.domain, "stop_cover", entity.entityId, null));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final CoverEntity entity = entityModel.entity;
List<Widget> buttons = [];
if (entity.supportOpen) {
buttons.add(
IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-up"),
size: entity.iconSize,
),
onPressed: entity.canBeOpened ? () =>_open(entity) : null
)
);
} else {
buttons.add(Container(width: entity.iconSize+20.0,));
}
if (entity.supportStop) {
buttons.add(
IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
size: entity.iconSize,
),
onPressed: () => _stop(entity)
)
);
} else {
buttons.add(Container(width: entity.iconSize+20.0,));
}
if (entity.supportClose) {
buttons.add(
IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-down"),
size: entity.iconSize,
),
onPressed: entity.canBeClosed ? () => _close(entity) : null
)
);
} else {
buttons.add(Container(width: entity.iconSize+20.0,));
}
return Row(
children: buttons,
);
}
}
class CoverEntityTiltControlState extends StatelessWidget {
void _open(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(entity.domain, "open_cover_tilt", entity.entityId, null));
}
void _close(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(entity.domain, "close_cover_tilt", entity.entityId, null));
}
void _stop(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(entity.domain, "stop_cover_tilt", entity.entityId, null));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final CoverEntity entity = entityModel.entity;
List<Widget> buttons = [];
if (entity.supportOpenTilt) {
buttons.add(
IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-top-right"),
size: entity.iconSize,
),
onPressed: entity.canTiltBeOpened ? () =>_open(entity) : null
)
);
} else {
buttons.add(Container(width: entity.iconSize+20.0,));
}
if (entity.supportStopTilt) {
buttons.add(
IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
size: entity.iconSize,
),
onPressed: () => _stop(entity)
)
);
} else {
buttons.add(Container(width: entity.iconSize+20.0,));
}
if (entity.supportCloseTilt) {
buttons.add(
IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-bottom-left"),
size: entity.iconSize,
),
onPressed: entity.canTiltBeClosed ? () => _close(entity) : null
)
);
} else {
buttons.add(Container(width: entity.iconSize+20.0,));
}
return Row(
children: buttons,
);
}
}

View File

@ -5,6 +5,8 @@ class EntityCollection {
Map<String, Entity> _entities; Map<String, Entity> _entities;
List<String> viewList; List<String> viewList;
bool get isEmpty => _entities.isEmpty;
EntityCollection() { EntityCollection() {
_entities = {}; _entities = {};
viewList = []; viewList = [];
@ -28,34 +30,37 @@ class EntityCollection {
Entity _createEntityInstance(rawEntityData) { Entity _createEntityInstance(rawEntityData) {
switch (rawEntityData["entity_id"].split(".")[0]) { switch (rawEntityData["entity_id"].split(".")[0]) {
case 'sun': {
return SunEntity(rawEntityData);
}
case "automation": case "automation":
case "input_boolean ": case "input_boolean":
case "switch": case "switch":
case "light": { case "light": {
return SwitchEntity(rawEntityData); return SwitchEntity(rawEntityData);
} }
case "script": case "script":
case "scene": { case "scene": {
return ButtonEntity(rawEntityData); return ButtonEntity(rawEntityData);
} }
case "input_datetime": { case "input_datetime": {
return DateTimeEntity(rawEntityData); return DateTimeEntity(rawEntityData);
} }
case "input_select": { case "input_select": {
return SelectEntity(rawEntityData); return SelectEntity(rawEntityData);
} }
case "input_number": { case "input_number": {
return SliderEntity(rawEntityData); return SliderEntity(rawEntityData);
} }
case "input_text": { case "input_text": {
return TextEntity(rawEntityData); return TextEntity(rawEntityData);
} }
case "climate": {
return ClimateEntity(rawEntityData);
}
case "cover": {
return CoverEntity(rawEntityData);
}
default: { default: {
return Entity(rawEntityData); return Entity(rawEntityData);
} }
@ -81,14 +86,24 @@ class EntityCollection {
} }
void updateFromRaw(Map rawEntityData) { void updateFromRaw(Map rawEntityData) {
//TODO pass entity in this function and call update from it get("${rawEntityData["entity_id"]}")?.update(rawEntityData);
_entities[rawEntityData["entity_id"]].update(rawEntityData);
} }
Entity get(String entityId) { Entity get(String entityId) {
return _entities[entityId]; return _entities[entityId];
} }
List<Entity> getAll(List ids) {
List<Entity> result = [];
ids.forEach((id){
Entity en = get(id);
if (en != null) {
result.add(en);
}
});
return result;
}
bool isExist(String entityId) { bool isExist(String entityId) {
return _entities[entityId] != null; return _entities[entityId] != null;
} }
@ -108,7 +123,7 @@ class EntityCollection {
entities.forEach((entiyId) { entities.forEach((entiyId) {
bool foundInGroup = false; bool foundInGroup = false;
result["userGroups"].forEach((userGroupId) { result["userGroups"].forEach((userGroupId) {
if (_entities[userGroupId].childEntities.contains(entiyId)) { if (_entities[userGroupId].childEntityIds.contains(entiyId)) {
foundInGroup = true; foundInGroup = true;
} }
}); });

View File

@ -1,118 +1,205 @@
part of 'main.dart'; part of 'main.dart';
class HomeAssistant { class HomeAssistant {
String _hassioAPIEndpoint; String _webSocketAPIEndpoint;
String _hassioPassword; String _password;
String _hassioAuthType; String _authType;
IOWebSocketChannel _hassioChannel; IOWebSocketChannel _hassioChannel;
SendMessageQueue _messageQueue;
int _currentMessageId = 0; int _currentMessageId = 0;
int _statesMessageId = 0; int _statesMessageId = 0;
int _servicesMessageId = 0; int _servicesMessageId = 0;
int _subscriptionMessageId = 0; int _subscriptionMessageId = 0;
int _configMessageId = 0; int _configMessageId = 0;
int _userInfoMessageId = 0;
EntityCollection _entities; EntityCollection _entities;
UIBuilder _uiBuilder; ViewBuilder _viewBuilder;
Map _instanceConfig = {}; Map _instanceConfig = {};
String _userName;
Completer _fetchCompleter; Completer _fetchCompleter;
Completer _statesCompleter; Completer _statesCompleter;
Completer _servicesCompleter; Completer _servicesCompleter;
Completer _configCompleter; Completer _configCompleter;
Timer _fetchingTimer; Completer _connectionCompleter;
Completer _userInfoCompleter;
Timer _connectionTimer;
Timer _fetchTimer;
bool autoReconnect = false;
StreamSubscription _socketSubscription;
int messageExpirationTime = 30; //seconds
Duration fetchTimeout = Duration(seconds: 30);
Duration connectTimeout = Duration(seconds: 15);
String get locationName => _instanceConfig["location_name"] ?? ""; String get locationName => _instanceConfig["location_name"] ?? "";
String get userName => _userName ?? locationName;
String get userAvatarText => userName.length > 0 ? userName[0] : "";
int get viewsCount => _entities.viewList.length ?? 0; int get viewsCount => _entities.viewList.length ?? 0;
UIBuilder get uiBuilder => _uiBuilder;
EntityCollection get entities => _entities; EntityCollection get entities => _entities;
HomeAssistant(String url, String password, String authType) { HomeAssistant() {
_hassioAPIEndpoint = url;
_hassioPassword = password;
_hassioAuthType = authType;
_entities = EntityCollection(); _entities = EntityCollection();
_uiBuilder = UIBuilder(); _messageQueue = SendMessageQueue(messageExpirationTime);
}
void updateConnectionSettings(String url, String password, String authType) {
_webSocketAPIEndpoint = url;
_password = password;
_authType = authType;
} }
Future fetch() { Future fetch() {
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) { if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
TheLogger.log("Warning","Previous fetch is not complited"); TheLogger.log("Warning","Previous fetch is not complited");
} else { } 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(); _fetchCompleter = new Completer();
_reConnectSocket().then((r) { _fetchTimer = Timer(fetchTimeout, () {
TheLogger.log("Error", "Data fetching timeout");
disconnect().then((_) {
_completeFetching({
"errorCode": 9,
"errorMessage": "Couldn't get data from server"
});
});
});
_connection().then((r) {
_getData(); _getData();
}).catchError((e) { }).catchError((e) {
_finishFetching(e); _completeFetching(e);
}); });
} }
return _fetchCompleter.future; return _fetchCompleter.future;
} }
closeConnection() { disconnect() async {
if (_hassioChannel?.closeCode == null) { if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) {
_hassioChannel?.sink?.close(); await _hassioChannel.sink.close().timeout(Duration(seconds: 3),
onTimeout: () => TheLogger.log("Debug", "Socket sink closed")
);
await _socketSubscription.cancel();
_hassioChannel = null;
} }
_hassioChannel = null;
} }
Future _reConnectSocket() { Future _connection() {
var _connectionCompleter = new Completer(); if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) { TheLogger.log("Debug","Previous connection is not complited");
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 { } else {
_connectionCompleter.complete(); if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
_connectionCompleter = new Completer();
autoReconnect = false;
disconnect().then((_){
TheLogger.log("Debug", "Socket connecting...");
_connectionTimer = Timer(connectTimeout, () {
TheLogger.log("Error", "Socket connection timeout");
_handleSocketError(null);
});
if (_socketSubscription != null) {
_socketSubscription.cancel();
}
_hassioChannel = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 30));
_socketSubscription = _hassioChannel.stream.listen(
(message) => _handleMessage(message),
cancelOnError: true,
onDone: () => _handleSocketClose(),
onError: (e) => _handleSocketError(e)
);
});
} else {
_completeConnecting(null);
}
} }
return _connectionCompleter.future; return _connectionCompleter.future;
} }
_getData() { void _handleSocketClose() {
_getConfig().then((result) { TheLogger.log("Debug","Socket disconnected. Automatic reconnect is $autoReconnect");
_getStates().then((result) { if (autoReconnect) {
_getServices().then((result) { _reconnect();
_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) { void _handleSocketError(e) {
TheLogger.log("Error","Socket stream Error: $e");
TheLogger.log("Debug","Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
} else {
disconnect().then((_) {
_completeConnecting({
"errorCode": 1,
"errorMessage": "Couldn't connect to Home Assistant. Check network connection or connection settings."
});
});
}
}
void _reconnect() {
disconnect().then((_) {
_connection().catchError((e){
_completeConnecting(e);
});
});
}
_getData() async {
List<Future> futures = [];
futures.add(_getStates());
futures.add(_getConfig());
futures.add(_getServices());
futures.add(_getUserInfo());
try {
await Future.wait(futures);
_completeFetching(null);
} catch (error) {
_completeFetching(error);
}
}
void _completeFetching(error) {
_fetchTimer.cancel();
_completeConnecting(error);
if (!_fetchCompleter.isCompleted) {
if (error != null) {
_fetchCompleter.completeError(error);
} else {
autoReconnect = true;
TheLogger.log("Debug", "Fetch complete successful");
_fetchCompleter.complete();
}
}
}
void _completeConnecting(error) {
_connectionTimer.cancel();
if (!_connectionCompleter.isCompleted) {
if (error != null) {
_connectionCompleter.completeError(error);
} else {
_connectionCompleter.complete();
}
} else if (error != null) {
eventBus.fire(ShowErrorEvent(error["errorMessage"], error["errorCode"]));
}
}
_handleMessage(String message) {
var data = json.decode(message); var data = json.decode(message);
//TheLogger.log("Debug","[Received] => Message type: ${data['type']}"); TheLogger.log("Debug","[Received] => ${data['type']}");
if (data["type"] == "auth_required") { if (data["type"] == "auth_required") {
_sendMessageRaw('{"type": "auth","$_hassioAuthType": "$_hassioPassword"}'); _sendAuthMessageRaw('{"type": "auth","$_authType": "$_password"}');
} else if (data["type"] == "auth_ok") { } else if (data["type"] == "auth_ok") {
_completeConnecting(null);
_sendSubscribe(); _sendSubscribe();
connectionCompleter.complete();
} else if (data["type"] == "auth_invalid") { } else if (data["type"] == "auth_invalid") {
connectionCompleter.completeError({"errorCode": 6, "errorMessage": "${data["message"]}"}); _completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") { } else if (data["type"] == "result") {
if (data["id"] == _configMessageId) { if (data["id"] == _configMessageId) {
_parseConfig(data); _parseConfig(data);
@ -120,6 +207,8 @@ class HomeAssistant {
_parseEntities(data); _parseEntities(data);
} else if (data["id"] == _servicesMessageId) { } else if (data["id"] == _servicesMessageId) {
_parseServices(data); _parseServices(data);
} else if (data["id"] == _userInfoMessageId) {
_parseUserInfo(data);
} else if (data["id"] == _currentMessageId) { } else if (data["id"] == _currentMessageId) {
TheLogger.log("Debug","Request id:$_currentMessageId was successful"); TheLogger.log("Debug","Request id:$_currentMessageId was successful");
} }
@ -139,14 +228,14 @@ class HomeAssistant {
void _sendSubscribe() { void _sendSubscribe() {
_incrementMessageId(); _incrementMessageId();
_subscriptionMessageId = _currentMessageId; _subscriptionMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}'); _sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
} }
Future _getConfig() { Future _getConfig() {
_configCompleter = new Completer(); _configCompleter = new Completer();
_incrementMessageId(); _incrementMessageId();
_configMessageId = _currentMessageId; _configMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}'); _sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}', false);
return _configCompleter.future; return _configCompleter.future;
} }
@ -155,16 +244,25 @@ class HomeAssistant {
_statesCompleter = new Completer(); _statesCompleter = new Completer();
_incrementMessageId(); _incrementMessageId();
_statesMessageId = _currentMessageId; _statesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}'); _sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}', false);
return _statesCompleter.future; return _statesCompleter.future;
} }
Future _getUserInfo() {
_userInfoCompleter = new Completer();
_incrementMessageId();
_userInfoMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_userInfoMessageId, "type": "auth/current_user"}', false);
return _userInfoCompleter.future;
}
Future _getServices() { Future _getServices() {
_servicesCompleter = new Completer(); _servicesCompleter = new Completer();
_incrementMessageId(); _incrementMessageId();
_servicesMessageId = _currentMessageId; _servicesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}'); _sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}', false);
return _servicesCompleter.future; return _servicesCompleter.future;
} }
@ -173,19 +271,51 @@ class HomeAssistant {
_currentMessageId += 1; _currentMessageId += 1;
} }
_sendMessageRaw(String message) { void _sendAuthMessageRaw(String message) {
if (message.indexOf('"type": "auth"') > 0) { TheLogger.log("Debug", "[Sending] ==> auth request");
TheLogger.log("Debug", "[Sending] ==> auth request");
} else {
TheLogger.log("Debug", "[Sending] ==> $message");
}
_hassioChannel.sink.add(message); _hassioChannel.sink.add(message);
} }
_sendMessageRaw(String message, bool queued) {
var sendCompleter = Completer();
if (queued) _messageQueue.add(message);
_connection().then((r) {
_messageQueue.getActualMessages().forEach((message){
TheLogger.log("Debug", "[Sending queued] ==> $message");
_hassioChannel.sink.add(message);
});
if (!queued) {
TheLogger.log("Debug", "[Sending] ==> $message");
_hassioChannel.sink.add(message);
}
sendCompleter.complete();
}).catchError((e){
sendCompleter.completeError(e);
});
return sendCompleter.future;
}
Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
_incrementMessageId();
String message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
if (additionalParams != null) {
additionalParams.forEach((name, value){
if ((value is double) || (value is int)) {
message += ', "$name" : $value';
} else {
message += ', "$name" : "$value"';
}
});
}
message += '}}';
return _sendMessageRaw(message, true);
}
void _handleEntityStateChange(Map eventData) { void _handleEntityStateChange(Map eventData) {
TheLogger.log("Debug", "New state for ${eventData['entity_id']}"); //TheLogger.log("Debug", "New state for ${eventData['entity_id']}");
_entities.updateState(eventData); Map data = Map.from(eventData);
eventBus.fire(new StateChangedEvent(eventData["entity_id"], null, false)); _entities.updateState(data);
eventBus.fire(new StateChangedEvent(data["entity_id"], null, false));
} }
void _parseConfig(Map data) { void _parseConfig(Map data) {
@ -197,6 +327,15 @@ class HomeAssistant {
} }
} }
void _parseUserInfo(Map data) {
if (data["success"] == true) {
_userName = data["result"]["name"];
} else {
_userName = null;
}
_userInfoCompleter.complete();
}
void _parseServices(response) { void _parseServices(response) {
_servicesCompleter.complete(); _servicesCompleter.complete();
/*if (response["success"] == false) { /*if (response["success"] == false) {
@ -229,32 +368,79 @@ class HomeAssistant {
return; return;
} }
_entities.parse(response["result"]); _entities.parse(response["result"]);
_uiBuilder.build(_entities); _viewBuilder = ViewBuilder(entityCollection: _entities);
_statesCompleter.complete(); _statesCompleter.complete();
} }
Future callService(String domain, String service, String entityId, Map<String, String> additionalParams) { Widget buildViews(BuildContext context) {
var sendCompleter = Completer(); return _viewBuilder.buildWidget(context);
//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"}); Future<List> getHistory(String entityId) async {
DateTime now = DateTime.now();
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
TheLogger.log("Debug", "$startTime");
String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId&skip_initial_state";
TheLogger.log("Debug", "$url");
http.Response historyResponse;
if (_authType == "access_token") {
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_password",
"Content-Type": "application/json"
});
} else {
historyResponse = await http.get(url, headers: {
"X-HA-Access": "$_password",
"Content-Type": "application/json"
});
}
var _history = json.decode(historyResponse.body);
if (_history is Map) {
return null;
} else if (_history is List) {
TheLogger.log("Debug", "${_history[0].toString()}");
return _history;
}
}
}
class SendMessageQueue {
int _messageTimeout;
List<HAMessage> _queue = [];
SendMessageQueue(this._messageTimeout);
void add(String message) {
_queue.add(HAMessage(_messageTimeout, message));
}
List<String> getActualMessages() {
_queue.removeWhere((item) => item.isExpired());
List<String> result = [];
_queue.forEach((haMessage){
result.add(haMessage.message);
}); });
_reConnectSocket().then((r) { this.clear();
_incrementMessageId(); return result;
String message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"'; }
if (additionalParams != null) {
additionalParams.forEach((name, value){ void clear() {
message += ', "$name" : "$value"'; _queue.clear();
}); }
}
message += '}}'; }
_sendMessageRaw(message);
_sendTimer.cancel(); class HAMessage {
sendCompleter.complete(); DateTime _timeStamp;
}).catchError((e){ int _messageTimeout;
_sendTimer.cancel(); String message;
sendCompleter.completeError(e);
}); HAMessage(this._messageTimeout, this.message) {
return sendCompleter.future; _timeStamp = DateTime.now();
}
bool isExpired() {
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
} }
} }

View File

@ -44,7 +44,7 @@ class _LogViewPageState extends State<LogViewPage> {
onPressed: () { onPressed: () {
String body = "```\n$_logData```"; String body = "```\n$_logData```";
String encodedBody = "${Uri.encodeFull(body)}"; String encodedBody = "${Uri.encodeFull(body)}";
haUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new?body=$encodedBody"); HAUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new?body=$encodedBody");
}, },
), ),
], ],

View File

@ -11,6 +11,11 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:date_format/date_format.dart'; import 'package:date_format/date_format.dart';
import 'package:http/http.dart' as http;
part 'entity_class/entity.class.dart';
part 'entity_class/stateless_widgets.dart';
part 'entity_class/stateful_widgets.dart';
part 'settings.page.dart'; part 'settings.page.dart';
part 'home_assistant.class.dart'; part 'home_assistant.class.dart';
@ -18,16 +23,14 @@ part 'log.page.dart';
part 'entity.page.dart'; part 'entity.page.dart';
part 'utils.class.dart'; part 'utils.class.dart';
part 'mdi.class.dart'; part 'mdi.class.dart';
part 'entity.class.dart';
part 'entity_collection.class.dart'; part 'entity_collection.class.dart';
part 'ui_builder_class.dart'; part 'view_builder.class.dart';
part 'view_class.dart'; part 'view_class.dart';
part 'card_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.2.1"; const appVersion = "0.3.0.38";
String homeAssistantWebHost; String homeAssistantWebHost;
@ -60,7 +63,7 @@ class HAClientApp 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") "/log-view": (context) => LogViewPage(title: "Log")
}, },
@ -81,345 +84,175 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
HomeAssistant _homeAssistant; HomeAssistant _homeAssistant;
EntityCollection _entities; EntityCollection _entities;
//Map _instanceConfig; //Map _instanceConfig;
String _webSocketApiEndpoint;
String _password;
String _authType;
int _uiViewsCount = 0; int _uiViewsCount = 0;
String _instanceHost; String _instanceHost;
int _errorCodeToBeShown = 0;
String _lastErrorMessage = "";
StreamSubscription _stateSubscription; StreamSubscription _stateSubscription;
StreamSubscription _settingsSubscription; StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription; StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription; StreamSubscription _showEntityPageSubscription;
bool _isLoading = true; StreamSubscription _refreshDataSubscription;
StreamSubscription _showErrorSubscription;
Map<String, Color> _badgeColors = { int _isLoading = 1;
"default": Color.fromRGBO(223, 76, 30, 1.0), bool _settingsLoaded = false;
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0) bool _accountMenuExpanded = false;
};
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_settingsLoaded = false;
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_homeAssistant = HomeAssistant();
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) { _settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
TheLogger.log("Debug","Settings change event: reconnect=${event.reconnect}"); TheLogger.log("Debug","Settings change event: reconnect=${event.reconnect}");
setState(() { if (event.reconnect) {
_errorCodeToBeShown = 0; _homeAssistant.disconnect().then((_){
}); _initialLoad();
_initConnection(); });
}
});
_initialLoad();
}
void _initialLoad() {
_loadConnectionSettings().then((_){
_subscribe();
_refreshData();
}, onError: (_) {
setState(() {
_isLoading = 2;
});
_showErrorSnackBar(message: _, errorCode: 5);
}); });
_initConnection();
} }
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
TheLogger.log("Debug","$state"); TheLogger.log("Debug","$state");
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed && _settingsLoaded) {
_refreshData(); _refreshData();
} }
} }
_initConnection() async { _loadConnectionSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
String domain = prefs.getString('hassio-domain'); String domain = prefs.getString('hassio-domain');
String port = prefs.getString('hassio-port'); String port = prefs.getString('hassio-port');
_instanceHost = "$domain:$port"; _instanceHost = "$domain:$port";
String apiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket"; _webSocketApiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port"; homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
String apiPassword = prefs.getString('hassio-password'); _password = prefs.getString('hassio-password');
String authType = prefs.getString('hassio-auth-type'); _authType = prefs.getString('hassio-auth-type');
if ((domain == null) || (port == null) || (apiPassword == null) || if ((domain == null) || (port == null) || (_password == null) ||
(domain.length == 0) || (port.length == 0) || (apiPassword.length == 0)) { (domain.length == 0) || (port.length == 0) || (_password.length == 0)) {
setState(() { throw("Check connection settings");
_errorCodeToBeShown = 5;
});
} else { } else {
if (_homeAssistant != null) _homeAssistant.closeConnection(); _settingsLoaded = true;
_createConnection(apiEndpoint, apiPassword, authType);
} }
} }
_createConnection(String apiEndpoint, String apiPassword, String authType) { _subscribe() {
_homeAssistant = HomeAssistant(apiEndpoint, apiPassword, authType); if (_stateSubscription == null) {
_refreshData(); _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (_stateSubscription != null) _stateSubscription.cancel(); setState(() {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { if (event.localChange) {
setState(() { _entities
if (event.localChange) { .get(event.entityId)
_entities .state = event.newState;
.get(event.entityId) }
.state = event.newState; });
}
}); });
}); }
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel(); if (_serviceCallSubscription == null) {
_serviceCallSubscription = eventBus.on<ServiceCallEvent>().listen((event) { _serviceCallSubscription =
_callService(event.domain, event.service, event.entityId, event.additionalParams); eventBus.on<ServiceCallEvent>().listen((event) {
}); _callService(event.domain, event.service, event.entityId,
event.additionalParams);
});
}
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel(); if (_showEntityPageSubscription == null) {
_showEntityPageSubscription = eventBus.on<ShowEntityPageEvent>().listen((event) { _showEntityPageSubscription =
_showEntityPage(event.entity); eventBus.on<ShowEntityPageEvent>().listen((event) {
}); _showEntityPage(event.entity);
});
}
if (_refreshDataSubscription == null) {
_refreshDataSubscription = eventBus.on<RefreshDataEvent>().listen((event){
_refreshData();
});
}
if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_showErrorSnackBar(message: event.text, errorCode: event.errorCode);
});
}
} }
_refreshData() async { _refreshData() async {
_homeAssistant.updateConnectionSettings(_webSocketApiEndpoint, _password, _authType);
setState(() { setState(() {
_isLoading = true; _hideErrorSnackBar();
_isLoading = 1;
}); });
_errorCodeToBeShown = 0; await _homeAssistant.fetch().then((result) {
if (_homeAssistant != null) { setState(() {
await _homeAssistant.fetch().then((result) { //_instanceConfig = _homeAssistant.instanceConfig;
setState(() { _entities = _homeAssistant.entities;
//_instanceConfig = _homeAssistant.instanceConfig; _uiViewsCount = _homeAssistant.viewsCount;
_entities = _homeAssistant.entities; _isLoading = 0;
_uiViewsCount = _homeAssistant.viewsCount;
_isLoading = false;
});
}).catchError((e) {
_setErrorState(e);
}); });
} }).catchError((e) {
_setErrorState(e);
});
eventBus.fire(RefreshDataFinishedEvent());
} }
_setErrorState(e) { _setErrorState(e) {
setState(() { setState(() {
_errorCodeToBeShown = e["errorCode"] != null ? e["errorCode"] : 99; _isLoading = 2;
_lastErrorMessage = e["errorMessage"] ?? "Unknown error";
_isLoading = false;
}); });
_showErrorSnackBar(
message: e != null ? e["errorMessage"] ?? "$e" : "Unknown error",
errorCode: e["errorCode"] != null ? e["errorCode"] : 99
);
} }
void _callService(String domain, String service, String entityId, Map<String, String> additionalParams) { void _callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
setState(() { _homeAssistant.callService(domain, service, entityId, additionalParams).catchError((e) => _setErrorState(e));
_isLoading = true;
});
_homeAssistant.callService(domain, service, entityId, additionalParams).then((r) {
setState(() {
_isLoading = false;
});
}).catchError((e) => _setErrorState(e));
} }
void _showEntityPage(Entity entity) { void _showEntityPage(Entity entity) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => EntityViewPage(entity: entity), builder: (context) => EntityViewPage(entity: entity, homeAssistant: _homeAssistant),
) )
); );
} }
List<Widget> _buildViews() {
List<Widget> result = [];
if ((_entities != null) && (!_homeAssistant.uiBuilder.isEmpty)) {
_homeAssistant.uiBuilder.views.forEach((viewId, view) {
result.add(
RefreshIndicator(
color: Colors.amber,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: _buildSingleView(view),
),
onRefresh: () => _refreshData(),
)
);
});
}
return result;
}
List<Widget> _buildSingleView(View view) {
List<Widget> result = [];
if (view.isThereBadges) {
result.add(
Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 1.0,
children: _buildBadges(view.badges),
)
);
}
view.cards.forEach((id, card) {
if (card.entities.isNotEmpty) {
result.add(_buildCard(card));
}
});
return result;
}
List<Widget> _buildBadges( Map<String, Badge> badges) {
List<Widget> result = [];
badges.forEach((id, badge) {
var badgeEntity = _entities.get(id);
if (badgeEntity != null) {
result.add(
_buildSingleBadge(badgeEntity)
);
}
});
return result;
}
Widget _buildSingleBadge(Entity data) {
double iconSize = 26.0;
Widget badgeIcon;
String badgeTextValue;
Color iconColor = _badgeColors[data.domain] ?? _badgeColors["default"];
switch (data.domain) {
case "sun": {
badgeIcon = data.state == "below_horizon" ?
Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
size: iconSize,
) :
Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8),
size: iconSize,
);
break;
}
case "sensor": {
badgeTextValue = data.unitOfMeasurement;
badgeIcon = Center(
child: Text(
"${data.state == 'unknown' ? '-' : data.state}",
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 17.0),
),
);
break;
}
case "device_tracker": {
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(data, iconSize,Colors.black);
badgeTextValue = data.state;
break;
}
default: {
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(data, iconSize,Colors.black);
}
}
Widget badgeText;
if (badgeTextValue == null || badgeTextValue.length == 0) {
badgeText = Container(width: 0.0, height: 0.0);
} else {
badgeText = Container(
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
child: Text("$badgeTextValue",
style: TextStyle(fontSize: 12.0, color: Colors.white),
textAlign: TextAlign.center, softWrap: false, overflow: TextOverflow.fade),
decoration: new BoxDecoration(
// Circle shape
//shape: BoxShape.circle,
color: iconColor,
borderRadius: BorderRadius.circular(9.0),
)
);
}
return Column(
children: <Widget>[
Container(
margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
width: 50.0,
height: 50.0,
decoration: new BoxDecoration(
// Circle shape
shape: BoxShape.circle,
color: Colors.white,
// The border you want
border: new Border.all(
width: 2.0,
color: iconColor,
),
),
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
width: 46.0,
height: 46.0,
top: 0.0,
left: 0.0,
child: badgeIcon,
),
Positioned(
//width: 50.0,
bottom: -9.0,
left: -10.0,
right: -10.0,
child: Center(
child: badgeText,
)
)
],
),
),
Container(
width: 60.0,
child: Text(
"${data.displayName}",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12.0),
softWrap: true,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
Card _buildCard(HACard card) {
List<Widget> body = [];
body.add(_buildCardHeader(card.friendlyName));
body.addAll(_buildCardBody(card.entities));
Card result = Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
);
return result;
}
Widget _buildCardHeader(String name) {
var result;
if (name.trim().length > 0) {
result = new ListTile(
//leading: const Icon(Icons.device_hub),
//subtitle: Text(".."),
//trailing: Text("${data["state"]}"),
title: Text("$name",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: 25.0)),
);
} else {
result = new Container(width: 0.0, height: 0.0);
}
return result;
}
List<Widget> _buildCardBody(List ids) {
List<Widget> entities = [];
ids.forEach((id) {
var entity = _entities.get(id);
if (entity != null) {
entities.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
child: entity.buildWidget(true, context),
));
}
});
return entities;
}
List<Tab> buildUIViewTabs() { List<Tab> buildUIViewTabs() {
//TODO move somewhere to ViewBuilder
List<Tab> result = []; List<Tab> result = [];
if ((_entities != null) && (!_homeAssistant.uiBuilder.isEmpty)) { if (!_entities.isEmpty) {
_homeAssistant.uiBuilder.views.forEach((viewId, view) { if (!_entities.hasDefaultView) {
result.add(
Tab(
icon:
Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
size: 24.0,
)
)
);
}
_entities.viewList.forEach((viewId) {
result.add( result.add(
Tab( Tab(
icon: MaterialDesignIcons.createIconWidgetFromEntityData(_entities.get(viewId), 24.0, null) ?? icon: MaterialDesignIcons.createIconWidgetFromEntityData(_entities.get(viewId), 24.0, null) ??
@ -438,7 +271,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
Row titleRow = Row( Row titleRow = Row(
children: [Text(_homeAssistant != null ? _homeAssistant.locationName : "")], children: [Text(_homeAssistant != null ? _homeAssistant.locationName : "")],
); );
if (_isLoading) { if (_isLoading == 1) {
titleRow.children.add(Padding( titleRow.children.add(Padding(
child: JumpingDotsProgressIndicator( child: JumpingDotsProgressIndicator(
fontSize: 26.0, fontSize: 26.0,
@ -446,58 +279,95 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
), ),
padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 30.0), padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 30.0),
)); ));
} else if (_isLoading == 2) {
titleRow.children.add(Padding(
child: Icon(
Icons.error_outline,
size: 20.0,
color: Colors.red,
),
padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 0.0),
));
} }
return titleRow; return titleRow;
} }
Drawer _buildAppDrawer() { Drawer _buildAppDrawer() {
List<Widget> menuItems = [];
menuItems.add(
UserAccountsDrawerHeader(
accountName: Text(_homeAssistant.userName),
accountEmail: Text(_instanceHost ?? "Not configured"),
onDetailsPressed: () {
setState(() {
_accountMenuExpanded = !_accountMenuExpanded;
});
},
currentAccountPicture: CircleAvatar(
child: Text(
_homeAssistant.userAvatarText,
style: TextStyle(
fontSize: 32.0
),
),
),
)
);
if (_accountMenuExpanded) {
menuItems.addAll([
ListTile(
leading: Icon(Icons.settings),
title: Text("Connection settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/connection-settings');
},
),
Divider(),
]);
} else {
menuItems.addAll([
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");
},
),
Divider(),
new AboutListTile(
applicationName: appName,
applicationVersion: appVersion,
applicationLegalese: "Keyboard Crumbs | www.keyboardcrumbs.io",
)
]);
}
return new Drawer( return new Drawer(
child: ListView( child: ListView(
children: <Widget>[ children: menuItems,
new UserAccountsDrawerHeader(
accountName: Text(_homeAssistant != null ? _homeAssistant.locationName : "Unknown"),
accountEmail: Text(_instanceHost ?? "Not configured"),
currentAccountPicture: new Image.asset('images/hassio-192x192.png'),
),
new ListTile(
leading: Icon(Icons.settings),
title: Text("Connection settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/connection-settings');
},
),
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(
applicationName: appName,
applicationVersion: appVersion,
applicationLegalese: "Keyboard Crumbs | www.keyboardcrumbs.io",
)
],
), ),
); );
} }
_checkShowInfo(BuildContext context) { void _hideErrorSnackBar() {
if (_errorCodeToBeShown > 0) { _scaffoldKey?.currentState?.hideCurrentSnackBar();
String message = _lastErrorMessage; }
void _showErrorSnackBar({Key key, @required String message, @required int errorCode}) {
SnackBarAction action; SnackBarAction action;
switch (_errorCodeToBeShown) { switch (errorCode) {
case 9:
case 11:
case 7:
case 1: { case 1: {
action = SnackBarAction( action = SnackBarAction(
label: "Retry", label: "Retry",
@ -532,9 +402,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
break; break;
} }
case 7: { case 10: {
action = SnackBarAction( action = SnackBarAction(
label: "Retry", label: "Refresh",
onPressed: () { onPressed: () {
_scaffoldKey?.currentState?.hideCurrentSnackBar(); _scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData(); _refreshData();
@ -554,19 +424,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
break; break;
} }
} }
Timer(Duration(seconds: 1), () { _scaffoldKey.currentState.hideCurrentSnackBar();
_scaffoldKey.currentState.hideCurrentSnackBar(); _scaffoldKey.currentState.showSnackBar(
_scaffoldKey.currentState.showSnackBar( SnackBar(
SnackBar( content: Text("$message (code: $errorCode)"),
content: Text("$message (code: $_errorCodeToBeShown)"), action: action,
action: action, duration: Duration(hours: 1),
duration: Duration(hours: 1), )
) );
);
});
} else {
_scaffoldKey?.currentState?.hideCurrentSnackBar();
}
} }
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
@ -576,7 +441,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
key: _scaffoldKey, key: _scaffoldKey,
appBar: AppBar( appBar: AppBar(
title: _buildAppTitle(), title: _buildAppTitle(),
bottom: empty ? null : TabBar(tabs: buildUIViewTabs()), leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
setState(() {
_accountMenuExpanded = false;
});
},
),
bottom: empty ? null : TabBar(
tabs: buildUIViewTabs(),
isScrollable: true,
),
), ),
drawer: _buildAppDrawer(), drawer: _buildAppDrawer(),
body: empty ? body: empty ?
@ -587,21 +464,18 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
Icon( Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"), MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
size: 100.0, size: 100.0,
color: _errorCodeToBeShown == 0 ? Colors.blue : Colors.redAccent, color: _isLoading == 2 ? Colors.redAccent : Colors.blue,
), ),
] ]
), ),
) )
: :
TabBarView( _homeAssistant.buildViews(context)
children: _buildViews()
),
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext 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 (_entities == null) {
return _buildScaffold(true); return _buildScaffold(true);
@ -620,7 +494,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
if (_settingsSubscription != null) _settingsSubscription.cancel(); if (_settingsSubscription != null) _settingsSubscription.cancel();
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel(); if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel(); if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
_homeAssistant.closeConnection(); if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
if (_showErrorSubscription != null) _showErrorSubscription.cancel();
_homeAssistant.disconnect();
super.dispose(); super.dispose();
} }
} }

View File

@ -16,7 +16,12 @@ class MaterialDesignIcons {
"input_text": "mdi:textbox", "input_text": "mdi:textbox",
"sun": "mdi:white-balance-sunny", "sun": "mdi:white-balance-sunny",
"scene": "mdi:google-pages", "scene": "mdi:google-pages",
"media_player": "mdi:cast" "media_player": "mdi:cast",
"climate": "mdi:thermostat",
"cover.open": "mdi:window-open",
"cover.closed": "mdi:window-closed",
"cover.closing": "mdi:window-open",
"cover.opening": "mdi:window-open",
}; };
static Map _defaultIconsByDeviceClass = { static Map _defaultIconsByDeviceClass = {
@ -68,7 +73,14 @@ class MaterialDesignIcons {
//"sensor.illuminance": "mdi:", //"sensor.illuminance": "mdi:",
"sensor.temperature": "mdi:thermometer", "sensor.temperature": "mdi:thermometer",
//"cover.window": "mdi:", //"cover.window": "mdi:",
//"cover.garage": "mdi:", "cover.garage.closed": "mdi:garage",
"cover.garage.open": "mdi:garage-open",
"cover.garage.opening": "mdi:garage-open",
"cover.garage.closing": "mdi:garage-open",
"cover.window.open": "mdi:window-open",
"cover.window.closed": "mdi:window-closed",
"cover.window.closing": "mdi:window-open",
"cover.window.opening": "mdi:window-open",
}; };
static Map _iconsDataMap = { static Map _iconsDataMap = {
"mdi:access-point": 0xf002, "mdi:access-point": 0xf002,
@ -2915,7 +2927,7 @@ class MaterialDesignIcons {
static int getDefaultIconByEntityId(String entityId, String deviceClass, String state) { static int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
String domain = entityId.split(".")[0]; String domain = entityId.split(".")[0];
String iconNameByDomain = _defaultIconsByDomains[domain]; String iconNameByDomain = _defaultIconsByDomains["$domain.$state"] ?? _defaultIconsByDomains["$domain"];
String iconNameByDeviceClass; String iconNameByDeviceClass;
if (deviceClass != null) { if (deviceClass != null) {
iconNameByDeviceClass = _defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? _defaultIconsByDeviceClass["$domain.$deviceClass"]; iconNameByDeviceClass = _defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? _defaultIconsByDeviceClass["$domain.$deviceClass"];

View File

@ -11,14 +11,29 @@ class ConnectionSettingsPage extends StatefulWidget {
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _hassioDomain = ""; String _hassioDomain = "";
String _hassioPort = "8123"; String _newHassioDomain = "";
String _hassioPort = "";
String _newHassioPort = "";
String _hassioPassword = ""; String _hassioPassword = "";
String _newHassioPassword = "";
String _socketProtocol = "wss"; String _socketProtocol = "wss";
String _newSocketProtocol = "wss";
String _authType = "access_token"; String _authType = "access_token";
String _newAuthType = "access_token";
bool _edited = false;
FocusNode _domainFocusNode;
FocusNode _portFocusNode;
FocusNode _passwordFocusNode;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_domainFocusNode = FocusNode();
_portFocusNode = FocusNode();
_passwordFocusNode = FocusNode();
_domainFocusNode.addListener(_checkConfigChanged);
_portFocusNode.addListener(_checkConfigChanged);
_passwordFocusNode.addListener(_checkConfigChanged);
_loadSettings(); _loadSettings();
} }
@ -26,25 +41,35 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() { setState(() {
_hassioDomain = prefs.getString("hassio-domain"); _hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
_hassioPort = prefs.getString("hassio-port") ?? '8123'; _hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_hassioPassword = prefs.getString("hassio-password"); _hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
_socketProtocol = prefs.getString("hassio-protocol") ?? 'wss'; _socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
_authType = prefs.getString("hassio-auth-type") ?? 'access_token'; _authType = _newAuthType = prefs.getString("hassio-auth-type") ?? 'access_token';
});
}
void _checkConfigChanged() {
setState(() {
_edited = ((_newHassioPassword != _hassioPassword) ||
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
(_newAuthType != _authType));
}); });
} }
_saveSettings() async { _saveSettings() async {
if (_hassioDomain.indexOf("http") == 0 && _hassioDomain.indexOf("//") > 0) { if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
_hassioDomain = _hassioDomain.split("//")[1]; _newHassioDomain = _newHassioDomain.split("//")[1];
} }
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("hassio-domain", _hassioDomain); prefs.setString("hassio-domain", _newHassioDomain);
prefs.setString("hassio-port", _hassioPort); prefs.setString("hassio-port", _newHassioPort);
prefs.setString("hassio-password", _hassioPassword); prefs.setString("hassio-password", _newHassioPassword);
prefs.setString("hassio-protocol", _socketProtocol); prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _socketProtocol == "wss" ? "https" : "http"); prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
prefs.setString("hassio-auth-type", _authType); prefs.setString("hassio-auth-type", _newAuthType);
} }
@override @override
@ -52,12 +77,20 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
return new Scaffold( return new Scaffold(
appBar: new AppBar( appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){ leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
_saveSettings().then((r){ Navigator.pop(context);
Navigator.pop(context);
});
eventBus.fire(SettingsChangedEvent(true));
}), }),
title: new Text(widget.title), title: new Text(widget.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.check),
onPressed: _edited ? (){
_saveSettings().then((r){
Navigator.pop(context);
eventBus.fire(SettingsChangedEvent(true));
});
} : null
)
],
), ),
body: ListView( body: ListView(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
@ -66,12 +99,10 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
children: [ children: [
Text("Use ssl (HTTPS)"), Text("Use ssl (HTTPS)"),
Switch( Switch(
value: (_socketProtocol == "wss"), value: (_newSocketProtocol == "wss"),
onChanged: (value) { onChanged: (value) {
setState(() { _newSocketProtocol = value ? "wss" : "ws";
_socketProtocol = value ? "wss" : "ws"; _checkConfigChanged();
});
_saveSettings();
}, },
) )
], ],
@ -80,36 +111,46 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
decoration: InputDecoration( decoration: InputDecoration(
labelText: "Home Assistant domain or ip address" labelText: "Home Assistant domain or ip address"
), ),
controller: TextEditingController( controller: new TextEditingController.fromValue(
text: _hassioDomain new TextEditingValue(
text: _newHassioDomain,
selection:
new TextSelection.collapsed(offset: _newHassioDomain.length)
)
), ),
onChanged: (value) { onChanged: (value) {
_hassioDomain = value; _newHassioDomain = value;
_saveSettings();
}, },
focusNode: _domainFocusNode,
onEditingComplete: _checkConfigChanged,
), ),
new TextField( new TextField(
decoration: InputDecoration( decoration: InputDecoration(
labelText: "Home Assistant port" labelText: "Home Assistant port (default is 8123)"
), ),
controller: TextEditingController( controller: new TextEditingController.fromValue(
text: _hassioPort new TextEditingValue(
text: _newHassioPort,
selection:
new TextSelection.collapsed(offset: _newHassioPort.length)
)
), ),
onChanged: (value) { onChanged: (value) {
_hassioPort = value; _newHassioPort = value;
_saveSettings(); //_saveSettings();
}, },
focusNode: _portFocusNode,
onEditingComplete: _checkConfigChanged,
), ),
new Row( new Row(
children: [ children: [
Text("Login with access token (HA >= 0.78.0)"), Text("Login with access token (HA >= 0.78.0)"),
Switch( Switch(
value: (_authType == "access_token"), value: (_newAuthType == "access_token"),
onChanged: (value) { onChanged: (value) {
setState(() { _newAuthType = value ? "access_token" : "api_password";
_authType = value ? "access_token" : "api_password"; _checkConfigChanged();
}); //_saveSettings();
_saveSettings();
}, },
) )
], ],
@ -118,16 +159,33 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
decoration: InputDecoration( decoration: InputDecoration(
labelText: _authType == "access_token" ? "Access token" : "API password" labelText: _authType == "access_token" ? "Access token" : "API password"
), ),
controller: TextEditingController( controller: new TextEditingController.fromValue(
text: _hassioPassword new TextEditingValue(
text: _newHassioPassword,
selection:
new TextSelection.collapsed(offset: _newHassioPassword.length)
)
), ),
onChanged: (value) { onChanged: (value) {
_hassioPassword = value; _newHassioPassword = value;
_saveSettings(); //_saveSettings();
}, },
focusNode: _passwordFocusNode,
onEditingComplete: _checkConfigChanged,
) )
], ],
), ),
); );
} }
@override
void dispose() {
_domainFocusNode.removeListener(_checkConfigChanged);
_portFocusNode.removeListener(_checkConfigChanged);
_passwordFocusNode.removeListener(_checkConfigChanged);
_domainFocusNode.dispose();
_portFocusNode.dispose();
_passwordFocusNode.dispose();
super.dispose();
}
} }

View File

@ -1,66 +0,0 @@
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;
});
}
}

View File

@ -32,7 +32,7 @@ class TheLogger {
} }
class haUtils { class HAUtils {
static void launchURL(String url) async { static void launchURL(String url) async {
if (await canLaunch(url)) { if (await canLaunch(url)) {
await launch(url); await launch(url);
@ -56,11 +56,19 @@ class SettingsChangedEvent {
SettingsChangedEvent(this.reconnect); SettingsChangedEvent(this.reconnect);
} }
class RefreshDataEvent {
RefreshDataEvent();
}
class RefreshDataFinishedEvent {
RefreshDataFinishedEvent();
}
class ServiceCallEvent { class ServiceCallEvent {
String domain; String domain;
String service; String service;
String entityId; String entityId;
Map<String, String> additionalParams; Map<String, dynamic> additionalParams;
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams); ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
} }
@ -69,4 +77,11 @@ class ShowEntityPageEvent {
Entity entity; Entity entity;
ShowEntityPageEvent(this.entity); ShowEntityPageEvent(this.entity);
}
class ShowErrorEvent {
String text;
int errorCode;
ShowErrorEvent(this.text, this.errorCode);
} }

101
lib/view_builder.class.dart Normal file
View File

@ -0,0 +1,101 @@
part of 'main.dart';
class ViewBuilder{
EntityCollection entityCollection;
List<View> _views;
ViewBuilder({
Key key,
this.entityCollection
}) {
_compose();
}
Widget buildWidget(BuildContext context) {
return ViewBuilderWidget(
entities: _views
);
}
void _compose() {
TheLogger.log("Debug", "Rebuilding all UI...");
_views = [];
if (!entityCollection.hasDefaultView) {
_views.add(_composeDefaultView());
}
_views.addAll(_composeViews());
}
View _composeDefaultView() {
Map<String, List<String>> userGroupsList = entityCollection.getDefaultViewTopLevelEntities();
List<Entity> entitiesForView = [];
userGroupsList["userGroups"].forEach((groupId){
entitiesForView.add(entityCollection.get(groupId));
});
userGroupsList["notGroupedEntities"].forEach((entityId){
entitiesForView.add(entityCollection.get(entityId));
});
return View(
entities: entitiesForView,
count: 0
);
}
List<View> _composeViews() {
List<View> result = [];
int counter = 0;
entityCollection.viewList.forEach((viewId) {
counter += 1;
//try {
Entity viewGroupEntity = entityCollection.get(viewId);
List<Entity> entitiesForView = [];
viewGroupEntity.childEntityIds.forEach((
entityId) { //Each entity or group in view
if (entityCollection.isExist(entityId)) {
Entity en = entityCollection.get(entityId);
if (en.isGroup) {
en.childEntities = entityCollection.getAll(en.childEntityIds);
}
entitiesForView.add(en);
} else {
TheLogger.log("Warning", "Unknown entity inside view: $entityId");
}
});
result.add(View(
count: counter,
entities: entitiesForView
));
/*} catch (error) {
TheLogger.log("Error","Error parsing view: $viewId");
}*/
});
return result;
}
}
class ViewBuilderWidget extends StatelessWidget {
final List<View> entities;
const ViewBuilderWidget({
Key key,
this.entities
}) : super(key: key);
@override
Widget build(BuildContext context) {
return TabBarView(
children: _buildChildren(context)
);
}
List<Widget> _buildChildren(BuildContext context) {
List<Widget> result = [];
entities.forEach((View view){
result.add(view.buildWidget(context));
});
return result;
}
}

View File

@ -1,53 +1,158 @@
part of 'main.dart'; part of 'main.dart';
class View { class View {
String _entityId; List<Entity> childEntitiesAsBadges;
int _count; Map<String, CardSkeleton> childEntitiesAsCards;
Map<String, HACard> cards;
Map<String, Badge> badges;
bool get isThereBadges => (badges != null) && (badges.isNotEmpty); int count;
List<Entity> entities;
View(String groupId, int viewCount) { View({
_entityId = groupId; Key key,
_count = viewCount; this.count,
cards = {}; this.entities
badges = {}; }) {
childEntitiesAsBadges = [];
childEntitiesAsCards = {};
_composeEntities();
} }
void add(Entity entity) { Widget buildWidget(BuildContext context) {
if (!entity.isGroup) { return ViewWidget(
_addEntityWithoutGroup(entity); badges: childEntitiesAsBadges,
} else { cards: childEntitiesAsCards,
_addCardWithEntities(entity); );
}
} }
void _addBadge(String entityId) { void _composeEntities() {
badges.addAll({entityId: Badge(entityId)}); entities.forEach((Entity entity){
} if (!entity.isGroup) {
if (entity.isBadge) {
void _addEntityWithoutGroup(Entity entity) { childEntitiesAsBadges.add(entity);
if (UIBuilder.isBadge(entity.domain)) { } else {
//This is badge String groupIdToAdd = "${entity.domain}.${entity.domain}$count";
_addBadge(entity.entityId); if (childEntitiesAsCards[groupIdToAdd] == null) {
} else { childEntitiesAsCards[groupIdToAdd] = CardSkeleton(
//This is a standalone entity displayName: entity.domain,
String groupIdToAdd = "${entity.domain}.${entity.domain}$_count"; );
if (cards[groupIdToAdd] == null) { }
_addCard(groupIdToAdd, entity.domain); childEntitiesAsCards[groupIdToAdd].childEntities.add(entity);
}
} else {
childEntitiesAsCards[entity.entityId] = CardSkeleton(
displayName: entity.displayName,
);
childEntitiesAsCards[entity.entityId].childEntities = entity.childEntities;
} }
cards[groupIdToAdd].addEntity(entity.entityId); });
}
}
class ViewWidget extends StatefulWidget {
final List<Entity> badges;
final Map<String, CardSkeleton> cards;
final String displayName;
const ViewWidget({
Key key,
this.badges,
this.cards,
this.displayName
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return ViewWidgetState();
}
}
class ViewWidgetState extends State<ViewWidget> {
StreamSubscription _refreshDataSubscription;
Completer _refreshCompleter;
@override
void initState() {
super.initState();
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) {
_refreshCompleter.complete();
}
});
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
color: Colors.amber,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: _buildChildren(context),
),
onRefresh: () => _refreshData(),
);
}
List<Widget> _buildChildren(BuildContext context) {
List<Widget> result = [];
if (widget.badges.isNotEmpty) {
result.insert(0,
Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 1.0,
children: _buildBadges(context, widget.badges),
)
);
} }
widget.cards.forEach((String id, CardSkeleton skeleton){
result.add(
HACard(
entities: skeleton.childEntities,
friendlyName: skeleton.displayName,
)
);
});
return result;
} }
void _addCard(String entityId, String friendlyName) { List<Widget> _buildBadges(BuildContext context, List<Entity> badges) {
cards.addAll({"$entityId": HACard(entityId, friendlyName)}); List<Widget> result = [];
badges.forEach((Entity entity) {
result.add(entity.buildBadgeWidget(context));
});
return result;
} }
void _addCardWithEntities(Entity entity) { Future _refreshData() {
cards.addAll({"${entity.entityId}": HACard(entity.entityId, entity.displayName)}); if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) {
cards[entity.entityId].addEntities(entity.childEntities); TheLogger.log("Debug","Previous data refresh is still in progress");
} else {
_refreshCompleter = Completer();
eventBus.fire(RefreshDataEvent());
}
return _refreshCompleter.future;
} }
@override
void dispose() {
_refreshDataSubscription.cancel();
super.dispose();
}
}
class CardSkeleton {
String displayName;
List<Entity> childEntities;
CardSkeleton({Key key, this.displayName, this.childEntities}) {
childEntities = [];
}
} }

View File

@ -114,7 +114,7 @@ packages:
source: hosted source: hosted
version: "0.1.2" version: "0.1.2"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct main" dependency: "direct dev"
description: description:
name: flutter_launcher_icons name: flutter_launcher_icons
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
@ -251,13 +251,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
package_info:
dependency: "direct main"
description:
name: package_info
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.2"
package_resolver: package_resolver:
dependency: transitive dependency: transitive
description: description:
@ -327,7 +320,7 @@ packages:
name: shared_preferences name: shared_preferences
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.2" version: "0.4.3"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@ -495,5 +488,5 @@ packages:
source: hosted source: hosted
version: "2.1.15" version: "2.1.15"
sdks: sdks:
dart: ">=2.0.0 <=2.1.0-dev.3.1.flutter-760a9690c2" dart: ">=2.0.0 <=2.1.0-dev.5.0.flutter-a2eb050044"
flutter: ">=0.1.4 <2.0.0" flutter: ">=0.1.4 <2.0.0"

View File

@ -1,7 +1,7 @@
name: hass_client name: hass_client
description: Home Assistant Android Client description: Home Assistant Android Client
version: 0.2.1+23 version: 0.3.0+38
environment: environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0" sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -12,8 +12,6 @@ dependencies:
shared_preferences: any shared_preferences: any
progress_indicators: ^0.1.2 progress_indicators: ^0.1.2
event_bus: ^1.0.1 event_bus: ^1.0.1
package_info: ^0.3.2
flutter_launcher_icons: ^0.6.1
cached_network_image: ^0.4.1 cached_network_image: ^0.4.1
url_launcher: ^3.0.3 url_launcher: ^3.0.3
date_format: ^1.0.5 date_format: ^1.0.5
@ -25,6 +23,7 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_launcher_icons: ^0.6.1
flutter_icons: flutter_icons:
android: true android: true