Compare commits

..

75 Commits
0.2.2 ... 0.3.3

Author SHA1 Message Date
988cd4a72f Version 0.3.3 2018-10-21 19:19:55 +03:00
d1ea916781 Fix assumed state switch 2018-10-21 19:18:33 +03:00
ce9f25b86c Light color button 2018-10-21 19:12:37 +03:00
f29762c931 Fix hidden group issue 2018-10-21 18:52:29 +03:00
30e4496ef1 Resolves #148 assumed_state support 2018-10-21 17:13:11 +03:00
7f9dc5dd3a Set Light britness to 0 if light is turned off 2018-10-21 16:18:27 +03:00
0f6babc243 Resolves #151 Group visibility support 2018-10-21 16:11:47 +03:00
6a43e04b31 Just small method rename 2018-10-21 15:26:14 +03:00
36fa5a50c4 Remove cancelling null subscription 2018-10-21 14:48:25 +03:00
9ad6d92ccd View entities in entityCollection. Child entities in parse 2018-10-21 14:43:52 +03:00
fafa8f43f4 Minor light fixes 2018-10-21 13:55:18 +03:00
9b490d33d5 Reverting views refactoring 2018-10-21 02:39:51 +03:00
33f9a1075e Remove ViewWrapper widget 2018-10-21 01:09:07 +03:00
b83006e2c3 View as widget refactoring 2018-10-21 00:30:58 +03:00
ba09c36bd2 Resloves #133 Light support 2018-10-18 23:47:55 +03:00
c71ee568b0 Merge pull request #152 from estevez-dev/release/0.3.2
Fix empty cards on default_view
2018-10-18 22:03:51 +03:00
75041f5c23 Fix empty cards on default_view 2018-10-18 21:57:10 +03:00
14da471774 Merge pull request #150 from estevez-dev/release/0.3.1
Resolves #136 cover state
2018-10-17 21:34:36 +03:00
369b44f1c8 Merge branch 'master' into release/0.3.1 2018-10-17 21:34:27 +03:00
8284bb6e76 Resolves #136 cover state 2018-10-17 21:21:00 +03:00
9b3b4dfbbc WIP #133 Lights 2018-10-17 02:19:46 +03:00
5ca4424933 Fix dropdown width 2018-10-16 23:30:17 +03:00
a308aa29a4 Add mode switch stateless widget 2018-10-16 23:20:27 +03:00
9e80b0eaaf Add temperature control stateless widget 2018-10-16 22:35:17 +03:00
85379cf491 Resolves #132 2018-10-16 21:10:59 +03:00
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
44 changed files with 3311 additions and 1145 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,61 @@
part of 'main.dart'; part of 'main.dart';
class HACard { class CardWidget 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 CardWidget({
_entityId = groupId; Key key,
_entities = []; this.entities,
_friendlyName = friendlyName; this.friendlyName
}) : super(key: key);
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
if (entityModel != null) {
final groupEntity = entityModel.entity;
if ((groupEntity!= null) && (groupEntity.isHidden)) {
return Container(width: 0.0, height: 0.0,);
}
}
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,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);
final Entity entity; final Entity entity;
final HomeAssistant homeAssistant;
@override @override
_EntityViewPageState createState() => new _EntityViewPageState(); _EntityViewPageState createState() => new _EntityViewPageState();
@ -11,26 +12,33 @@ 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();
_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,7 +52,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
), ),
body: Padding( body: Padding(
padding: EdgeInsets.all(10.0), padding: EdgeInsets.all(10.0),
child: _entity.buildWidget(context, false) child: widget.entity.buildEntityPageWidget(context)
), ),
); );
} }

View File

@ -1,24 +0,0 @@
part of '../main.dart';
class _ButtonEntityWidgetState extends _EntityWidgetState {
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "turn_on", widget.entity.entityId, null));
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
return FlatButton(
onPressed: (() {
sendNewState(null);
}),
child: Text(
"EXECUTE",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Entity.STATE_FONT_SIZE, color: Colors.blue),
),
);
}
}

View File

@ -1,92 +0,0 @@
part of '../main.dart';
class _DateTimeEntityWidgetState extends _EntityWidgetState {
bool get hasDate => widget.entity._attributes["has_date"] ?? false;
bool get hasTime => widget.entity._attributes["has_time"] ?? false;
int get year => widget.entity._attributes["year"] ?? 1970;
int get month => widget.entity._attributes["month"] ?? 1;
int get day => widget.entity._attributes["day"] ?? 1;
int get hour => widget.entity._attributes["hour"] ?? 0;
int get minute => widget.entity._attributes["minute"] ?? 0;
int get second => widget.entity._attributes["second"] ?? 0;
String get formattedState => _getFormattedState();
DateTime get dateTimeState => _getDateTimeState();
DateTime _getDateTimeState() {
return DateTime(this.year, this.month, this.day, this.hour, this.minute, this.second);
}
String _getFormattedState() {
String formattedState = "";
if (this.hasDate) {
formattedState += formatDate(dateTimeState, [M, ' ', d, ', ', yyyy]);
}
if (this.hasTime) {
formattedState += " "+formatDate(dateTimeState, [HH, ':', nn]);
}
return formattedState;
}
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_datetime", widget.entity.entityId,
newValue));
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
return Padding(
padding:
EdgeInsets.fromLTRB(0.0, 0.0, Entity.RIGHT_WIDGET_PADDING, 0.0),
child: GestureDetector(
child: Text(
"$formattedState",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.STATE_FONT_SIZE,
)),
onTap: () => _handleStateTap(context),
)
);
}
void _handleStateTap(BuildContext context) {
if (hasDate) {
_showDatePicker(context).then((date) {
if (date != null) {
if (hasTime) {
_showTimePicker(context).then((time){
sendNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}", "time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
});
} else {
sendNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}"});
}
}
});
} else if (hasTime) {
_showTimePicker(context).then((time){
if (time != null) {
sendNewState({"time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
}
});
} else {
TheLogger.log("Warning", "${widget.entity.entityId} has no date and no time");
}
}
Future _showDatePicker(BuildContext context) {
return showDatePicker(
context: context,
initialDate: dateTimeState,
firstDate: DateTime(1970),
lastDate: DateTime(2037) //Unix timestamp will finish at Jan 19, 2038
);
}
Future _showTimePicker(BuildContext context) {
return showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(dateTimeState)
);
}
}

View File

@ -4,64 +4,157 @@ class Entity {
static const STATE_ICONS_COLORS = { static const STATE_ICONS_COLORS = {
"on": Colors.amber, "on": Colors.amber,
"off": Color.fromRGBO(68, 115, 158, 1.0), "off": Color.fromRGBO(68, 115, 158, 1.0),
"default": Color.fromRGBO(68, 115, 158, 1.0),
"unavailable": Colors.black12, "unavailable": Colors.black12,
"unknown": Colors.black12, "unknown": Colors.black12,
"playing": Colors.amber "playing": Colors.amber
}; };
static const RIGHT_WIDGET_PADDING = 14.0; static const badgeColors = {
static const LEFT_WIDGET_PADDING = 8.0; "default": Color.fromRGBO(223, 76, 30, 1.0),
static const EXTENDED_WIDGET_HEIGHT = 50.0; "binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
static const WIDGET_HEIGHT = 34.0; };
static const ICON_SIZE = 28.0; static List badgeDomains = [
static const STATE_FONT_SIZE = 16.0; "alarm_control_panel",
static const NAME_FONT_SIZE = 16.0; "binary_sensor",
static const SMALL_FONT_SIZE = 14.0; "device_tracker",
static const INPUT_WIDTH = 160.0; "updater",
"sun",
"timer",
"sensor"
];
Map _attributes; static const rightWidgetPadding = 14.0;
String _domain; static const leftWidgetPadding = 8.0;
String _entityId; static const extendedWidgetHeight = 50.0;
String _state; static const iconSize = 28.0;
static const stateFontSize = 16.0;
static const nameFontSize = 16.0;
static const smallFontSize = 14.0;
static const largeFontSize = 24.0;
static const inputWidth = 160.0;
static const rowPadding = 10.0;
double widgetHeight = 34.0;
Map attributes;
String domain;
String entityId;
String state;
String assumedState;
DateTime _lastUpdated; DateTime _lastUpdated;
String get displayName => List<Entity> childEntities = [];
_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; List<String> attributesToShow = ["all"];
String get displayName =>
attributes["friendly_name"] ?? (attributes["name"] ?? "_");
String get deviceClass => attributes["device_class"] ?? null;
bool get isView => bool get isView =>
(_domain == "group") && (domain == "group") &&
(_attributes != null ? _attributes["view"] ?? false : false); (attributes != null ? attributes["view"] ?? false : false);
bool get isGroup => _domain == "group"; bool get isGroup => domain == "group";
String get icon => _attributes["icon"] ?? ""; bool get isBadge => Entity.badgeDomains.contains(domain);
String get icon => attributes["icon"] ?? "";
bool get isOn => state == "on"; bool get isOn => state == "on";
String get entityPicture => _attributes["entity_picture"]; String get entityPicture => attributes["entity_picture"];
String get unitOfMeasurement => _attributes["unit_of_measurement"] ?? ""; String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
List get childEntities => _attributes["entity_id"] ?? []; List get childEntityIds => attributes["entity_id"] ?? [];
String get lastUpdated => _getLastUpdatedFormatted(); String get lastUpdated => _getLastUpdatedFormatted();
bool get isHidden => attributes["hidden"] ?? false;
Entity(Map rawData) { Entity(Map rawData) {
update(rawData); update(rawData);
} }
void update(Map rawData) { void update(Map rawData) {
_attributes = rawData["attributes"] ?? {}; attributes = rawData["attributes"] ?? {};
_domain = rawData["entity_id"].split(".")[0]; domain = rawData["entity_id"].split(".")[0];
_entityId = rawData["entity_id"]; entityId = rawData["entity_id"];
_state = rawData["state"]; state = rawData["state"];
assumedState = state;
_lastUpdated = DateTime.tryParse(rawData["last_updated"]); _lastUpdated = DateTime.tryParse(rawData["last_updated"]);
} }
EntityWidget buildWidget(BuildContext context, bool inCard) { double _getDoubleAttributeValue(String attributeName) {
return EntityWidget( var temp1 = attributes["$attributeName"];
if (temp1 is int) {
return temp1.toDouble();
} else if (temp1 is double) {
return temp1;
} else {
return double.tryParse("$temp1");
}
}
int _getIntAttributeValue(String attributeName) {
var temp1 = attributes["$attributeName"];
if (temp1 is int) {
return temp1;
} else if (temp1 is double) {
return temp1.round();
} else {
return int.tryParse("$temp1");
}
}
Widget buildDefaultWidget(BuildContext context) {
return EntityModel(
entity: this, entity: this,
inCard: inCard, child: DefaultEntityContainer(
state: _buildStatePart(context),
height: widgetHeight,
),
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), height: widgetHeight),
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() { String _getLastUpdatedFormatted() {
if (_lastUpdated == null) { if (_lastUpdated == null) {
return "-"; return "-";
@ -90,151 +183,380 @@ class Entity {
return "$v $text"; return "$v $text";
} }
} }
} }
class EntityWidget extends StatefulWidget { class SwitchEntity extends Entity {
EntityWidget({Key key, this.entity, this.inCard}) : super(key: key); SwitchEntity(Map rawData) : super(rawData);
final Entity entity;
final bool inCard;
@override @override
_EntityWidgetState createState() { Widget _buildStatePart(BuildContext context) {
switch (entity.domain) { return SwitchStateWidget();
case "automation":
case "input_boolean ":
case "switch":
case "light": {
return _SwitchEntityWidgetState();
}
case "script":
case "scene": {
return _ButtonEntityWidgetState();
}
case "input_datetime": {
return _DateTimeEntityWidgetState();
}
case "input_select": {
return _SelectEntityWidgetState();
}
case "input_number": {
return _SliderEntityWidgetState();
}
case "input_text": {
return _TextEntityWidgetState();
}
default: {
return _EntityWidgetState();
}
}
} }
} }
class _EntityWidgetState extends State<EntityWidget> { class ButtonEntity extends Entity {
ButtonEntity(Map rawData) : super(rawData);
@override @override
Widget build(BuildContext context) { Widget _buildStatePart(BuildContext context) {
if (widget.inCard) { return ButtonStateWidget();
return _buildMainWidget(context);
} else {
return ListView(
children: <Widget>[
_buildMainWidget(context),
_buildLastUpdatedWidget()
],
);
}
} }
}
Widget _buildMainWidget(BuildContext context) { class TextEntity extends Entity {
return SizedBox( TextEntity(Map rawData) : super(rawData);
height: Entity.WIDGET_HEIGHT,
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 TextInputStateWidget();
}
}
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( child: Row(
children: <Widget>[ children: <Widget>[
GestureDetector( SliderStateWidget(
child: _buildIconWidget(), expanded: true,
onTap: widget.inCard ? openEntityPage : null,
), ),
Expanded( SimpleEntityState(),
child: GestureDetector(
child: _buildNameWidget(),
onTap: widget.inCard ? openEntityPage : null,
),
),
_buildActionWidget(widget.inCard, context)
], ],
), ),
); );
} }
void openEntityPage() { @override
eventBus.fire(new ShowEntityPageEvent(widget.entity)); Widget _buildStatePartForPage(BuildContext context) {
return SimpleEntityState();
} }
void sendNewState(newState) { @override
return; Widget _buildAdditionalControlsForPage(BuildContext context) {
} return SliderStateWidget(
expanded: false,
Widget buildAdditionalWidget() {
return _buildLastUpdatedWidget();
}
Widget _buildIconWidget() {
return Padding(
padding: EdgeInsets.fromLTRB(Entity.LEFT_WIDGET_PADDING, 0.0, 12.0, 0.0),
child: MaterialDesignIcons.createIconWidgetFromEntityData(
widget.entity,
Entity.ICON_SIZE,
Entity.STATE_ICONS_COLORS[widget.entity.state] ?? Colors.blueGrey),
);
}
Widget _buildLastUpdatedWidget() {
return Padding(
padding: EdgeInsets.fromLTRB(
Entity.LEFT_WIDGET_PADDING, Entity.SMALL_FONT_SIZE, 0.0, 0.0),
child: Text(
'${widget.entity.lastUpdated}',
textAlign: TextAlign.left,
style:
TextStyle(fontSize: Entity.SMALL_FONT_SIZE, color: Colors.black26),
),
);
}
Widget _buildNameWidget() {
return Padding(
padding: EdgeInsets.only(right: 10.0),
child: Text(
"${widget.entity.displayName}",
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle(fontSize: Entity.NAME_FONT_SIZE),
),
);
}
Widget _buildActionWidget(bool inCard, BuildContext context) {
return Padding(
padding:
EdgeInsets.fromLTRB(0.0, 0.0, Entity.RIGHT_WIDGET_PADDING, 0.0),
child: GestureDetector(
child: Text(
"${widget.entity.state}${widget.entity.unitOfMeasurement}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.STATE_FONT_SIZE,
)),
onTap: openEntityPage,
)
); );
} }
} }
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 != "opening") && (state != "open"));
bool get canBeClosed => ((state != "closing") && (state != "closed"));
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();
}
}
class LightEntity extends Entity {
static const SUPPORT_BRIGHTNESS = 1;
static const SUPPORT_COLOR_TEMP = 2;
static const SUPPORT_EFFECT = 4;
static const SUPPORT_FLASH = 8;
static const SUPPORT_COLOR = 16;
static const SUPPORT_TRANSITION = 32;
static const SUPPORT_WHITE_VALUE = 128;
bool get supportBrightness => ((attributes["supported_features"] &
LightEntity.SUPPORT_BRIGHTNESS) ==
LightEntity.SUPPORT_BRIGHTNESS);
bool get supportColorTemp => ((attributes["supported_features"] &
LightEntity.SUPPORT_COLOR_TEMP) ==
LightEntity.SUPPORT_COLOR_TEMP);
bool get supportEffect => ((attributes["supported_features"] &
LightEntity.SUPPORT_EFFECT) ==
LightEntity.SUPPORT_EFFECT);
bool get supportFlash => ((attributes["supported_features"] &
LightEntity.SUPPORT_FLASH) ==
LightEntity.SUPPORT_FLASH);
bool get supportColor => ((attributes["supported_features"] &
LightEntity.SUPPORT_COLOR) ==
LightEntity.SUPPORT_COLOR);
bool get supportTransition => ((attributes["supported_features"] &
LightEntity.SUPPORT_TRANSITION) ==
LightEntity.SUPPORT_TRANSITION);
bool get supportWhiteValue => ((attributes["supported_features"] &
LightEntity.SUPPORT_WHITE_VALUE) ==
LightEntity.SUPPORT_WHITE_VALUE);
int get brightness => _getIntAttributeValue("brightness");
int get colorTemp => _getIntAttributeValue("color_temp");
double get maxMireds => _getDoubleAttributeValue("max_mireds");
double get minMireds => _getDoubleAttributeValue("min_mireds");
Color get color => _getColor();
bool get isAdditionalControls => ((attributes["supported_features"] != null) && (attributes["supported_features"] != 0));
List<String> get effectList => _getEffectList();
LightEntity(Map rawData) : super(rawData);
Color _getColor() {
List rgb = attributes["rgb_color"];
try {
if ((rgb != null) && (rgb.length > 0)) {
return Color.fromARGB(255, rgb[0], rgb[1], rgb[2]);
} else {
return null;
}
} catch (e) {
return null;
}
}
List<String> _getEffectList() {
if (attributes["effect_list"] != null) {
List<String> result = (attributes["effect_list"] as List).cast<String>();
return result;
} else {
return null;
}
}
@override
Widget _buildStatePart(BuildContext context) {
return SwitchStateWidget();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
if (!isAdditionalControls) {
return Container(height: 0.0, width: 0.0);
} else {
return LightControlsWidget();
}
}
}

View File

@ -1,36 +0,0 @@
part of '../main.dart';
class _SelectEntityWidgetState extends _EntityWidgetState {
List<String> _listOptions = [];
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "select_option", widget.entity.entityId,
{"option": "$newValue"}));
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
_listOptions.clear();
if (widget.entity._attributes["options"] != null) {
widget.entity._attributes["options"].forEach((value){
_listOptions.add(value.toString());
});
}
return Container(
width: Entity.INPUT_WIDTH,
child: DropdownButton<String>(
value: widget.entity.state,
items: this._listOptions.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (_) {
sendNewState(_);
},
),
);
}
}

View File

@ -1,65 +0,0 @@
part of '../main.dart';
class _SliderEntityWidgetState extends _EntityWidgetState {
int _multiplier = 1;
double get minValue => widget.entity._attributes["min"] ?? 0.0;
double get maxValue => widget.entity._attributes["max"] ?? 100.0;
double get valueStep => widget.entity._attributes["step"] ?? 1.0;
double get doubleState => double.tryParse(widget.entity.state) ?? 0.0;
@override
void initState() {
super.initState();
}
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_value", widget.entity.entityId,
{"value": "${newValue.toString()}"}));
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
if (valueStep < 1) {
_multiplier = 10;
} else if (valueStep < 0.1) {
_multiplier = 100;
}
return Container(
width: 200.0,
child: Row(
children: <Widget>[
Expanded(
child: Slider(
min: this.minValue * _multiplier,
max: this.maxValue * _multiplier,
value: (doubleState <= this.maxValue) &&
(doubleState >= this.minValue)
? doubleState * _multiplier
: this.minValue * _multiplier,
onChanged: (value) {
setState(() {
widget.entity.state = (value.roundToDouble() / _multiplier).toString();
});
/*eventBus.fire(new StateChangedEvent(widget.entity.entityId,
(value.roundToDouble() / _multiplier).toString(), true));*/
},
onChangeEnd: (value) {
sendNewState(value.roundToDouble() / _multiplier);
},
),
),
Padding(
padding: EdgeInsets.only(right: Entity.RIGHT_WIDGET_PADDING),
child: Text("${widget.entity.state}${widget.entity.unitOfMeasurement}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.STATE_FONT_SIZE,
)),
)
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,775 @@
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,
@required this.height
}) : super(key: key);
final Widget state;
final double height;
@override
Widget build(BuildContext context) {
return SizedBox(
height: height,
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, Entity.rightWidgetPadding, 0.0),
child: GestureDetector(
child: Text(
"${entityModel.entity.state}${entityModel.entity.unitOfMeasurement}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: 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: 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(
Entity.leftWidgetPadding, 0.0, 12.0, 0.0),
//TODO: move createIconWidgetFromEntityData into this widget
child: MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entity,
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(
Entity.leftWidgetPadding, 0.0, 0.0, 0.0),
child: Text(
'${entityModel.entity.lastUpdated}',
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 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("$name", "$value"));
});
} else {
entityModel.entity.attributesToShow.forEach((String attr) {
String attrValue = entityModel.entity.getAttribute("$attr");
if (attrValue != null) {
attrs.add(
_buildSingleAttribute("$attr", "$attrValue"));
}
});
}
return Column(
children: attrs,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
);
}
Widget _buildSingleAttribute(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;
String targetTemp = "-";
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
targetTemp = "${entity.temperature}";
} else if ((entity.supportTargetTemperatureLow) &&
(entity.targetLow != null)) {
targetTemp = "${entity.targetLow}";
if ((entity.supportTargetTemperatureHigh) &&
(entity.targetHigh != null)) {
targetTemp += " - ${entity.targetHigh}";
}
}
return Padding(
padding: EdgeInsets.fromLTRB(
0.0, 0.0, Entity.rightWidgetPadding, 0.0),
child: GestureDetector(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: <Widget>[
Text("${entity.state}",
textAlign: TextAlign.right,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: Entity.stateFontSize,
)),
Text(" $targetTemp",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.stateFontSize,
))
],
),
entity.attributes["current_temperature"] != null ?
Text("Currently: ${entity.attributes["current_temperature"]}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.stateFontSize,
color: Colors.black45)
) :
Container(height: 0.0,)
],
),
onTap: () => entityModel.handleTap
? eventBus.fire(new ShowEntityPageEvent(entity))
: null,
));
}
}
class TemperatureControlWidget extends StatelessWidget {
final double value;
final double fontSize;
final Color fontColor;
final onSmallInc;
final onLargeInc;
final onSmallDec;
final onLargeDec;
TemperatureControlWidget(
{Key key,
@required this.value,
@required this.onSmallInc,
@required this.onSmallDec,
@required this.onLargeInc,
@required this.onLargeDec,
this.fontSize,
this.fontColor})
: super(key: key);
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
"$value",
style: TextStyle(
fontSize: fontSize ?? 24.0,
color: fontColor ?? Colors.black
),
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
'mdi:chevron-up')),
iconSize: 30.0,
onPressed: () => onSmallInc(),
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
'mdi:chevron-down')),
iconSize: 30.0,
onPressed: () => onSmallDec(),
)
],
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
'mdi:chevron-double-up')),
iconSize: 30.0,
onPressed: () => onLargeInc(),
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
'mdi:chevron-double-down')),
iconSize: 30.0,
onPressed: () => onLargeDec(),
)
],
)
],
);
}
}
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 CoverEntityTiltControlButtons 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,
);
}
}
class ButtonStateWidget extends StatelessWidget {
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: Entity.stateFontSize, color: Colors.blue),
),
);
}
}
class ModeSelectorWidget extends StatelessWidget {
final String caption;
final List<String> options;
final String value;
final double captionFontSize;
final double valueFontSize;
final double bottomPadding;
final onChange;
ModeSelectorWidget({
Key key,
this.caption,
@required this.options,
this.value,
@required this.onChange,
this.captionFontSize,
this.valueFontSize,
this.bottomPadding
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("$caption", style: TextStyle(
fontSize: captionFontSize ?? Entity.stateFontSize
)),
Row(
children: <Widget>[
Expanded(
child: ButtonTheme(
alignedDropdown: true,
child: DropdownButton<String>(
value: value,
iconSize: 30.0,
isExpanded: true,
style: TextStyle(
fontSize: valueFontSize ?? Entity.largeFontSize,
color: Colors.black,
),
hint: Text("Select ${caption.toLowerCase()}"),
items: options.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (mode) => onChange(mode),
),
),
)
],
),
Container(height: bottomPadding ?? Entity.rowPadding,)
],
);
}
}
class ModeSwitchWidget extends StatelessWidget {
final String caption;
final onChange;
final double captionFontSize;
final bool value;
ModeSwitchWidget({
Key key,
@required this.caption,
@required this.onChange,
this.captionFontSize,
this.value
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: Text(
"$caption",
style: TextStyle(
fontSize: captionFontSize ?? Entity.stateFontSize
),
),
),
Switch(
onChanged: (value) => onChange(value),
value: value ?? false,
)
],
);
}
}

View File

@ -1,26 +0,0 @@
part of '../main.dart';
class _SwitchEntityWidgetState extends _EntityWidgetState {
@override
void initState() {
super.initState();
}
@override
void sendNewState(newValue) {
eventBus.fire(new ServiceCallEvent(
widget.entity.domain, (newValue as bool) ? "turn_on" : "turn_off", widget.entity.entityId, null));
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
return Switch(
value: widget.entity.isOn,
onChanged: ((switchState) {
sendNewState(switchState);
widget.entity.state = switchState ? 'on' : 'off';
}),
);
}
}

View File

@ -1,83 +0,0 @@
part of '../main.dart';
class _TextEntityWidgetState extends _EntityWidgetState {
String _tmpValue;
FocusNode _focusNode = FocusNode();
bool validValue = false;
int get valueMinLength => widget.entity._attributes["min"] ?? -1;
int get valueMaxLength => widget.entity._attributes["max"] ?? -1;
String get valuePattern => widget.entity._attributes["pattern"] ?? null;
bool get isTextField => widget.entity._attributes["mode"] == "text";
bool get isPasswordField => widget.entity._attributes["mode"] == "password";
@override
void initState() {
super.initState();
_focusNode.addListener(_focusListener);
_tmpValue = widget.entity.state;
}
@override
void sendNewState(newValue) {
if (validate(newValue)) {
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_value", widget.entity.entityId,
{"value": "$newValue"}));
} else {
setState(() {
_tmpValue = widget.entity.state;
});
}
}
bool validate(newValue) {
if (newValue is String) {
validValue = (newValue.length >= this.valueMinLength) &&
(this.valueMaxLength == -1 ||
(newValue.length <= this.valueMaxLength));
} else {
validValue = true;
}
return validValue;
}
void _focusListener() {
if (!_focusNode.hasFocus && (_tmpValue != widget.entity.state)) {
sendNewState(_tmpValue);
}
}
@override
Widget _buildActionWidget(bool inCard, BuildContext context) {
if (!_focusNode.hasFocus && (_tmpValue != widget.entity.state)) {
_tmpValue = widget.entity.state;
}
if (this.isTextField || this.isPasswordField) {
return Container(
width: Entity.INPUT_WIDTH,
child: TextField(
focusNode: _focusNode,
obscureText: this.isPasswordField,
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _tmpValue,
selection:
new TextSelection.collapsed(offset: _tmpValue.length))),
onChanged: (value) {
_tmpValue = value;
}),
);
} else {
TheLogger.log("Warning", "Unsupported input mode for ${widget.entity.entityId}");
return super._buildActionWidget(inCard, context);
}
}
@override
void dispose() {
_focusNode.removeListener(_focusListener);
_focusNode.dispose();
super.dispose();
}
}

View File

@ -2,33 +2,76 @@ part of 'main.dart';
class EntityCollection { class EntityCollection {
Map<String, Entity> _entities; Map<String, Entity> _allEntities;
List<String> viewList; Map<String, Entity> views;
bool get isEmpty => _allEntities.isEmpty;
EntityCollection() { EntityCollection() {
_entities = {}; _allEntities = {};
viewList = []; views = {};
} }
bool get hasDefaultView => _entities["group.default_view"] != null; bool get hasDefaultView => _allEntities["group.default_view"] != null;
void parse(List rawData) { void parse(List rawData) {
_entities.clear(); _allEntities.clear();
viewList.clear(); views.clear();
TheLogger.log("Debug","Parsing ${rawData.length} Home Assistant entities"); TheLogger.log("Debug","Parsing ${rawData.length} Home Assistant entities");
rawData.forEach((rawEntityData) { rawData.forEach((rawEntityData) {
Entity newEntity = addFromRaw(rawEntityData); addFromRaw(rawEntityData);
});
if (newEntity.isView) { _allEntities.forEach((entityId, entity){
viewList.add(newEntity.entityId); if ((entity.isGroup) && (entity.childEntityIds != null)) {
entity.childEntities = getAll(entity.childEntityIds);
}
if (entity.isView) {
views[entityId] = entity;
} }
}); });
} }
Entity _createEntityInstance(rawEntityData) { Entity _createEntityInstance(rawEntityData) {
switch (rawEntityData["entity_id"].split(".")[0]) {
case 'sun': {
return SunEntity(rawEntityData);
}
case "automation":
case "input_boolean":
case "switch": {
return SwitchEntity(rawEntityData);
}
case "light": {
return LightEntity(rawEntityData);
}
case "script":
case "scene": {
return ButtonEntity(rawEntityData);
}
case "input_datetime": {
return DateTimeEntity(rawEntityData);
}
case "input_select": {
return SelectEntity(rawEntityData);
}
case "input_number": {
return SliderEntity(rawEntityData);
}
case "input_text": {
return TextEntity(rawEntityData);
}
case "climate": {
return ClimateEntity(rawEntityData);
}
case "cover": {
return CoverEntity(rawEntityData);
}
default: {
return Entity(rawEntityData); return Entity(rawEntityData);
} }
}
}
void updateState(Map rawStateData) { void updateState(Map rawStateData) {
if (isExist(rawStateData["entity_id"])) { if (isExist(rawStateData["entity_id"])) {
@ -39,32 +82,42 @@ class EntityCollection {
} }
void add(Entity entity) { void add(Entity entity) {
_entities[entity.entityId] = entity; _allEntities[entity.entityId] = entity;
} }
Entity addFromRaw(Map rawEntityData) { Entity addFromRaw(Map rawEntityData) {
Entity entity = _createEntityInstance(rawEntityData); Entity entity = _createEntityInstance(rawEntityData);
_entities[entity.entityId] = entity; _allEntities[entity.entityId] = entity;
return entity; return entity;
} }
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 _allEntities[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 _allEntities[entityId] != null;
} }
Map<String,List<String>> getDefaultViewTopLevelEntities() { Map<String,List<String>> getDefaultViewTopLevelEntities() {
Map<String,List<String>> result = {"userGroups": [], "notGroupedEntities": []}; Map<String,List<String>> result = {"userGroups": [], "notGroupedEntities": []};
List<String> entities = []; List<String> entities = [];
_entities.forEach((id, entity){ _allEntities.forEach((id, entity){
if ((id.indexOf("group.") == 0) && (id.indexOf(".all_") == -1) && (!entity.isView)) { if ((id.indexOf("group.") == 0) && (id.indexOf(".all_") == -1) && (!entity.isView)) {
result["userGroups"].add(id); result["userGroups"].add(id);
} }
@ -76,7 +129,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 (_allEntities[userGroupId].childEntityIds.contains(entiyId)) {
foundInGroup = true; foundInGroup = true;
} }
}); });

View File

@ -1,118 +1,204 @@
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"] ?? "";
int get viewsCount => _entities.viewList.length ?? 0; String get userName => _userName ?? locationName;
UIBuilder get uiBuilder => _uiBuilder; String get userAvatarText => userName.length > 0 ? userName[0] : "";
int get viewsCount => _entities.views.length ?? 0;
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() { }
var _connectionCompleter = new Completer();
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) { Future _connection() {
TheLogger.log("Debug","Socket connecting..."); if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
_hassioChannel = IOWebSocketChannel.connect(_hassioAPIEndpoint); TheLogger.log("Debug","Previous connection is not complited");
_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);
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."
}); });
}).catchError((e) {
_finishFetching(e);
}); });
}).catchError((e) { }
_finishFetching(e); }
void _reconnect() {
disconnect().then((_) {
_connection().catchError((e){
_completeConnecting(e);
});
}); });
} }
_finishFetching(error) { _getData() async {
_fetchingTimer.cancel(); 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) { if (error != null) {
_fetchCompleter.completeError(error); _fetchCompleter.completeError(error);
} else { } else {
autoReconnect = true;
TheLogger.log("Debug", "Fetch complete successful");
_fetchCompleter.complete(); _fetchCompleter.complete();
} }
} }
}
_handleMessage(Completer connectionCompleter, String message) { 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']}");
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,11 +206,14 @@ 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","[Received] => Request id:$_currentMessageId was successful");
} }
} else if (data["type"] == "event") { } else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) { if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
TheLogger.log("Debug","[Received] => ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
_handleEntityStateChange(data["event"]["data"]); _handleEntityStateChange(data["event"]["data"]);
} else if (data["event"] != null) { } else if (data["event"] != null) {
TheLogger.log("Warning","Unhandled event type: ${data["event"]["event_type"]}"); TheLogger.log("Warning","Unhandled event type: ${data["event"]["event_type"]}");
@ -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) || (value is List)) {
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"
}); });
_reConnectSocket().then((r) { } else {
_incrementMessageId(); historyResponse = await http.get(url, headers: {
String message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"'; "X-HA-Access": "$_password",
if (additionalParams != null) { "Content-Type": "application/json"
additionalParams.forEach((name, value){
message += ', "$name" : "$value"';
}); });
} }
message += '}}'; var _history = json.decode(historyResponse.body);
_sendMessageRaw(message); if (_history is Map) {
_sendTimer.cancel(); return null;
sendCompleter.complete(); } else if (_history is List) {
}).catchError((e){ TheLogger.log("Debug", "${_history[0].toString()}");
_sendTimer.cancel(); return _history;
sendCompleter.completeError(e); }
}); }
return sendCompleter.future; }
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);
});
this.clear();
return result;
}
void clear() {
_queue.clear();
}
}
class HAMessage {
DateTime _timeStamp;
int _messageTimeout;
String message;
HAMessage(this._messageTimeout, this.message) {
_timeStamp = DateTime.now();
}
bool isExpired() {
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
} }
} }

View File

@ -11,14 +11,12 @@ 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;
import 'package:flutter_colorpicker/material_picker.dart';
part 'entity_class/entity.class.dart'; part 'entity_class/entity.class.dart';
part 'entity_class/button_entity.class.dart'; part 'entity_class/stateless_widgets.dart';
part 'entity_class/datetime_entity.class.dart'; part 'entity_class/stateful_widgets.dart';
part 'entity_class/select_entity.class.dart';
part 'entity_class/slider_entity.class.dart';
part 'entity_class/switch_entity.class.dart';
part 'entity_class/text_entity.class.dart';
part 'settings.page.dart'; part 'settings.page.dart';
part 'home_assistant.class.dart'; part 'home_assistant.class.dart';
@ -27,14 +25,13 @@ part 'entity.page.dart';
part 'utils.class.dart'; part 'utils.class.dart';
part 'mdi.class.dart'; part 'mdi.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.2"; const appVersion = "0.3.3";
String homeAssistantWebHost; String homeAssistantWebHost;
@ -88,67 +85,79 @@ 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}");
if (event.reconnect) {
_homeAssistant.disconnect().then((_){
_initialLoad();
});
}
});
_initialLoad();
}
void _initialLoad() {
_loadConnectionSettings().then((_){
_subscribe();
_refreshData();
}, onError: (_) {
setState(() { setState(() {
_errorCodeToBeShown = 0; _isLoading = 2;
}); });
_initConnection(); _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();
if (_stateSubscription != null) _stateSubscription.cancel();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
setState(() { setState(() {
if (event.localChange) { if (event.localChange) {
@ -158,278 +167,96 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
} }
}); });
}); });
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel(); }
_serviceCallSubscription = eventBus.on<ServiceCallEvent>().listen((event) { if (_serviceCallSubscription == null) {
_callService(event.domain, event.service, event.entityId, event.additionalParams); _serviceCallSubscription =
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 =
eventBus.on<ShowEntityPageEvent>().listen((event) {
_showEntityPage(event.entity); _showEntityPage(event.entity);
}); });
} }
_refreshData() async { if (_refreshDataSubscription == null) {
setState(() { _refreshDataSubscription = eventBus.on<RefreshDataEvent>().listen((event){
_isLoading = true; _refreshData();
});
}
if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_showErrorSnackBar(message: event.text, errorCode: event.errorCode);
});
}
}
_refreshData() async {
_homeAssistant.updateConnectionSettings(_webSocketApiEndpoint, _password, _authType);
setState(() {
_hideErrorSnackBar();
_isLoading = 1;
}); });
_errorCodeToBeShown = 0;
if (_homeAssistant != null) {
await _homeAssistant.fetch().then((result) { await _homeAssistant.fetch().then((result) {
setState(() { setState(() {
//_instanceConfig = _homeAssistant.instanceConfig; //_instanceConfig = _homeAssistant.instanceConfig;
_entities = _homeAssistant.entities; _entities = _homeAssistant.entities;
_uiViewsCount = _homeAssistant.viewsCount; _uiViewsCount = _homeAssistant.viewsCount;
_isLoading = false; TheLogger.log("Debug","_uiViewsCount=$_uiViewsCount");
_isLoading = 0;
}); });
}).catchError((e) { }).catchError((e) {
_setErrorState(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(context, true),
));
}
});
return entities;
}
List<Tab> buildUIViewTabs() { List<Tab> buildUIViewTabs() {
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( result.add(
Tab( Tab(
icon: MaterialDesignIcons.createIconWidgetFromEntityData(_entities.get(viewId), 24.0, null) ?? icon:
Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
size: 24.0,
)
)
);
}
_entities.views.forEach((viewId, groupEntity) {
result.add(
Tab(
icon: MaterialDesignIcons.createIconWidgetFromEntityData(groupEntity, 24.0, null) ??
Icon( Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"), MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
size: 24.0, size: 24.0,
@ -445,7 +272,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,
@ -453,20 +280,43 @@ 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() {
return new Drawer( List<Widget> menuItems = [];
child: ListView( menuItems.add(
children: <Widget>[ UserAccountsDrawerHeader(
new UserAccountsDrawerHeader( accountName: Text(_homeAssistant.userName),
accountName: Text(_homeAssistant != null ? _homeAssistant.locationName : "Unknown"),
accountEmail: Text(_instanceHost ?? "Not configured"), accountEmail: Text(_instanceHost ?? "Not configured"),
currentAccountPicture: new Image.asset('images/hassio-192x192.png'), onDetailsPressed: () {
setState(() {
_accountMenuExpanded = !_accountMenuExpanded;
});
},
currentAccountPicture: CircleAvatar(
child: Text(
_homeAssistant.userAvatarText,
style: TextStyle(
fontSize: 32.0
), ),
new ListTile( ),
),
)
);
if (_accountMenuExpanded) {
menuItems.addAll([
ListTile(
leading: Icon(Icons.settings), leading: Icon(Icons.settings),
title: Text("Connection settings"), title: Text("Connection settings"),
onTap: () { onTap: () {
@ -474,6 +324,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
Navigator.of(context).pushNamed('/connection-settings'); Navigator.of(context).pushNamed('/connection-settings');
}, },
), ),
Divider(),
]);
} else {
menuItems.addAll([
new ListTile( new ListTile(
leading: Icon(Icons.insert_drive_file), leading: Icon(Icons.insert_drive_file),
title: Text("Log"), title: Text("Log"),
@ -490,21 +344,31 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
HAUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new"); HAUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new");
}, },
), ),
Divider(),
new AboutListTile( new AboutListTile(
applicationName: appName, applicationName: appName,
applicationVersion: appVersion, applicationVersion: appVersion,
applicationLegalese: "Keyboard Crumbs | www.keyboardcrumbs.io", applicationLegalese: "Keyboard Crumbs | www.keyboardcrumbs.io",
) )
], ]);
}
return new Drawer(
child: ListView(
children: menuItems,
), ),
); );
} }
_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",
@ -539,9 +403,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();
@ -561,19 +425,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: $_errorCodeToBeShown)"), content: Text("$message (code: $errorCode)"),
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>();
@ -583,7 +442,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 ?
@ -594,21 +465,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);
@ -627,7 +495,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

@ -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);
} }
@ -70,3 +78,10 @@ class ShowEntityPageEvent {
ShowEntityPageEvent(this.entity); ShowEntityPageEvent(this.entity);
} }
class ShowErrorEvent {
String text;
int errorCode;
ShowErrorEvent(this.text, this.errorCode);
}

View File

@ -0,0 +1,88 @@
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){
Entity en = entityCollection.get(groupId);
entitiesForView.add(en);
});
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.views.forEach((viewId, viewGroupEntity) {
counter += 1;
//try {
result.add(View(
count: counter,
entities: viewGroupEntity.childEntities
));
/*} 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,168 @@
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 = {};
_filterEntities();
} }
void add(Entity entity) { Widget buildWidget(BuildContext context) {
return ViewWidget(
badges: childEntitiesAsBadges,
cards: childEntitiesAsCards,
);
}
void _filterEntities() {
entities.forEach((Entity entity){
if (!entity.isGroup) { if (!entity.isGroup) {
_addEntityWithoutGroup(entity); if (entity.isBadge) {
childEntitiesAsBadges.add(entity);
} else { } else {
_addCardWithEntities(entity); String groupIdToAdd = "${entity.domain}.${entity.domain}$count";
if (childEntitiesAsCards[groupIdToAdd] == null) {
childEntitiesAsCards[groupIdToAdd] = CardSkeleton(
displayName: entity.domain,
);
} }
childEntitiesAsCards[groupIdToAdd].childEntities.add(entity);
} }
void _addBadge(String entityId) {
badges.addAll({entityId: Badge(entityId)});
}
void _addEntityWithoutGroup(Entity entity) {
if (UIBuilder.isBadge(entity.domain)) {
//This is badge
_addBadge(entity.entityId);
} else { } else {
//This is a standalone entity childEntitiesAsCards[entity.entityId] = CardSkeleton(
String groupIdToAdd = "${entity.domain}.${entity.domain}$_count"; displayName: entity.displayName,
if (cards[groupIdToAdd] == null) { groupEntity: entity
_addCard(groupIdToAdd, entity.domain); );
childEntitiesAsCards[entity.entityId].childEntities = entity.childEntities;
} }
cards[groupIdToAdd].addEntity(entity.entityId); });
}
}
void _addCard(String entityId, String friendlyName) {
cards.addAll({"$entityId": HACard(entityId, friendlyName)});
}
void _addCardWithEntities(Entity entity) {
cards.addAll({"${entity.entityId}": HACard(entity.entityId, entity.displayName)});
cards[entity.entityId].addEntities(entity.childEntities);
} }
} }
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(
EntityModel(
entity: skeleton.groupEntity,
handleTap: false,
child: CardWidget(
entities: skeleton.childEntities,
friendlyName: skeleton.displayName,
)
)
);
});
return result;
}
List<Widget> _buildBadges(BuildContext context, List<Entity> badges) {
List<Widget> result = [];
badges.forEach((Entity entity) {
result.add(entity.buildBadgeWidget(context));
});
return result;
}
Future _refreshData() {
if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) {
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;
Entity groupEntity;
CardSkeleton({
Key key,
this.displayName,
this.childEntities,
this.groupEntity}) {
childEntities = [];
}
}

View File

@ -113,8 +113,15 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.2" version: "0.1.2"
flutter_launcher_icons: flutter_colorpicker:
dependency: "direct main" dependency: "direct main"
description:
name: flutter_colorpicker
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
flutter_launcher_icons:
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 +258,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 +327,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 +495,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.2+24 version: 0.3.3+43
environment: environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0" sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -12,11 +12,10 @@ 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
flutter_colorpicker: ^0.1.0
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
@ -25,6 +24,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