Compare commits
125 Commits
0.1.2-alph
...
0.3.3
Author | SHA1 | Date | |
---|---|---|---|
988cd4a72f | |||
d1ea916781 | |||
ce9f25b86c | |||
f29762c931 | |||
30e4496ef1 | |||
7f9dc5dd3a | |||
0f6babc243 | |||
6a43e04b31 | |||
36fa5a50c4 | |||
9ad6d92ccd | |||
fafa8f43f4 | |||
9b490d33d5 | |||
33f9a1075e | |||
b83006e2c3 | |||
ba09c36bd2 | |||
c71ee568b0 | |||
75041f5c23 | |||
14da471774 | |||
369b44f1c8 | |||
8284bb6e76 | |||
9b3b4dfbbc | |||
5ca4424933 | |||
a308aa29a4 | |||
9e80b0eaaf | |||
85379cf491 | |||
758376a891 | |||
2ebba364e3 | |||
6e604440c0 | |||
c23034688e | |||
69f45b52cf | |||
ffc053fbe6 | |||
b5f9ecf601 | |||
948d1d4e23 | |||
136297c18b | |||
164800951d | |||
84d283de2b | |||
2fa35d771a | |||
326cd073b9 | |||
e99c3f5742 | |||
16a9392fa6 | |||
5bf063969b | |||
c19a0511a6 | |||
a4ac40b366 | |||
ce69f044fb | |||
70b6469bd1 | |||
253316fb1f | |||
ec71200ab0 | |||
bc1f4eab2e | |||
4085006446 | |||
b7fb821abe | |||
284e7ba451 | |||
17a3bd8d35 | |||
c2b88c8a12 | |||
c975af4c79 | |||
debf1b71f1 | |||
4725953b32 | |||
e7ca1209e2 | |||
f9afa663f5 | |||
5068cbbcf4 | |||
043d3a9905 | |||
77c5f80c13 | |||
e0d35d07dc | |||
285447a5b7 | |||
ed3e4ba272 | |||
908563063a | |||
7f2611b410 | |||
648750655c | |||
8a0d5581d9 | |||
98d716109b | |||
ebb2f2b4e5 | |||
d910e4dd43 | |||
95d80fbbfc | |||
41297150c2 | |||
b14b248f2f | |||
13fc1bff27 | |||
eee8f21e76 | |||
8ce3560d8d | |||
9e97bac85b | |||
4a0b447f00 | |||
bc4969dae8 | |||
5025b3d384 | |||
0d7e7eb6f7 | |||
062392b38c | |||
acd468ae75 | |||
60f216df13 | |||
9de8a659d3 | |||
7dd8f65af7 | |||
9e83a3e447 | |||
2f135169a9 | |||
76d2750ad6 | |||
571778fbd4 | |||
b89b5dfb98 | |||
a196b0d8d4 | |||
95f7c14296 | |||
2fcd27d240 | |||
6834f2ca34 | |||
c0a9b89d40 | |||
067ccfde02 | |||
4b4fc338f6 | |||
08c07e8398 | |||
df04d000b2 | |||
d0d1ab2740 | |||
af3a5bc611 | |||
b935a0e372 | |||
49444ab3df | |||
098a556279 | |||
375ae36884 | |||
0b42019ef3 | |||
516d38a8a9 | |||
fb886a4622 | |||
662b44d443 | |||
f9c48e6cc7 | |||
88d6e1008f | |||
4540fadf1e | |||
bd13d3693d | |||
5db9d6005f | |||
7e4f744598 | |||
772b569da5 | |||
0e11c1a146 | |||
60793dbf89 | |||
2b622cff04 | |||
94bcc30421 | |||
94f43ded6f | |||
7f7be8aa78 | |||
c0e0059487 |
@ -39,8 +39,8 @@ android {
|
|||||||
applicationId "com.keyboardcrumbs.haclient"
|
applicationId "com.keyboardcrumbs.haclient"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 27
|
targetSdkVersion 27
|
||||||
versionCode 19
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName "0.1.1-alpha"
|
versionName flutterVersionName
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 711 B After Width: | Height: | Size: 715 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.3 KiB |
61
lib/card_class.dart
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class CardWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
final List<Entity> entities;
|
||||||
|
final String friendlyName;
|
||||||
|
|
||||||
|
const CardWidget({
|
||||||
|
Key key,
|
||||||
|
this.entities,
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCardHeader() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildCardBody(BuildContext context) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
65
lib/entity.page.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class EntityViewPage extends StatefulWidget {
|
||||||
|
EntityViewPage({Key key, @required this.entity, @required this.homeAssistant }) : super(key: key);
|
||||||
|
|
||||||
|
final Entity entity;
|
||||||
|
final HomeAssistant homeAssistant;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_EntityViewPageState createState() => new _EntityViewPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EntityViewPageState extends State<EntityViewPage> {
|
||||||
|
String _title;
|
||||||
|
StreamSubscription _stateSubscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||||
|
if (event.entityId == widget.entity.entityId) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_prepareData();
|
||||||
|
_getHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _prepareData() async {
|
||||||
|
_title = widget.entity.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _getHistory() {
|
||||||
|
/* widget.homeAssistant.getHistory(widget.entity.entityId).then((List history) {
|
||||||
|
if (history != null) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new Scaffold(
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
|
// Here we take the value from the MyHomePage object that was created by
|
||||||
|
// the App.build method, and use it to set our appbar title.
|
||||||
|
title: new Text(_title),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.all(10.0),
|
||||||
|
child: widget.entity.buildEntityPageWidget(context)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose(){
|
||||||
|
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
562
lib/entity_class/entity.class.dart
Normal file
@ -0,0 +1,562 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class Entity {
|
||||||
|
static const STATE_ICONS_COLORS = {
|
||||||
|
"on": Colors.amber,
|
||||||
|
"off": Color.fromRGBO(68, 115, 158, 1.0),
|
||||||
|
"default": Color.fromRGBO(68, 115, 158, 1.0),
|
||||||
|
"unavailable": Colors.black12,
|
||||||
|
"unknown": Colors.black12,
|
||||||
|
"playing": Colors.amber
|
||||||
|
};
|
||||||
|
static const badgeColors = {
|
||||||
|
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
||||||
|
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
||||||
|
};
|
||||||
|
static List badgeDomains = [
|
||||||
|
"alarm_control_panel",
|
||||||
|
"binary_sensor",
|
||||||
|
"device_tracker",
|
||||||
|
"updater",
|
||||||
|
"sun",
|
||||||
|
"timer",
|
||||||
|
"sensor"
|
||||||
|
];
|
||||||
|
|
||||||
|
static const rightWidgetPadding = 14.0;
|
||||||
|
static const leftWidgetPadding = 8.0;
|
||||||
|
static const extendedWidgetHeight = 50.0;
|
||||||
|
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;
|
||||||
|
|
||||||
|
List<Entity> childEntities = [];
|
||||||
|
|
||||||
|
List<String> attributesToShow = ["all"];
|
||||||
|
|
||||||
|
String get displayName =>
|
||||||
|
attributes["friendly_name"] ?? (attributes["name"] ?? "_");
|
||||||
|
|
||||||
|
String get deviceClass => attributes["device_class"] ?? null;
|
||||||
|
bool get isView =>
|
||||||
|
(domain == "group") &&
|
||||||
|
(attributes != null ? attributes["view"] ?? false : false);
|
||||||
|
bool get isGroup => domain == "group";
|
||||||
|
bool get isBadge => Entity.badgeDomains.contains(domain);
|
||||||
|
String get icon => attributes["icon"] ?? "";
|
||||||
|
bool get isOn => state == "on";
|
||||||
|
String get entityPicture => attributes["entity_picture"];
|
||||||
|
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
|
||||||
|
List get childEntityIds => attributes["entity_id"] ?? [];
|
||||||
|
String get lastUpdated => _getLastUpdatedFormatted();
|
||||||
|
bool get isHidden => attributes["hidden"] ?? false;
|
||||||
|
|
||||||
|
Entity(Map rawData) {
|
||||||
|
update(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(Map rawData) {
|
||||||
|
attributes = rawData["attributes"] ?? {};
|
||||||
|
domain = rawData["entity_id"].split(".")[0];
|
||||||
|
entityId = rawData["entity_id"];
|
||||||
|
state = rawData["state"];
|
||||||
|
assumedState = state;
|
||||||
|
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getDoubleAttributeValue(String attributeName) {
|
||||||
|
var temp1 = attributes["$attributeName"];
|
||||||
|
if (temp1 is int) {
|
||||||
|
return temp1.toDouble();
|
||||||
|
} else if (temp1 is double) {
|
||||||
|
return temp1;
|
||||||
|
} else {
|
||||||
|
return 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,
|
||||||
|
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() {
|
||||||
|
if (_lastUpdated == null) {
|
||||||
|
return "-";
|
||||||
|
} else {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
Duration d = now.difference(_lastUpdated);
|
||||||
|
String text;
|
||||||
|
int v;
|
||||||
|
if (d.inDays == 0) {
|
||||||
|
if (d.inHours == 0) {
|
||||||
|
if (d.inMinutes == 0) {
|
||||||
|
text = "seconds ago";
|
||||||
|
v = d.inSeconds;
|
||||||
|
} else {
|
||||||
|
text = "minutes ago";
|
||||||
|
v = d.inMinutes;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = "hours ago";
|
||||||
|
v = d.inHours;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = "days ago";
|
||||||
|
v = d.inDays;
|
||||||
|
}
|
||||||
|
return "$v $text";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SwitchEntity extends Entity {
|
||||||
|
SwitchEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return SwitchStateWidget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ButtonEntity extends Entity {
|
||||||
|
ButtonEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return ButtonStateWidget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextEntity extends Entity {
|
||||||
|
TextEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
int get valueMinLength => attributes["min"] ?? -1;
|
||||||
|
int get valueMaxLength => attributes["max"] ?? -1;
|
||||||
|
String get valuePattern => attributes["pattern"] ?? null;
|
||||||
|
bool get isTextField => attributes["mode"] == "text";
|
||||||
|
bool get isPasswordField => attributes["mode"] == "password";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return 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(
|
||||||
|
children: <Widget>[
|
||||||
|
SliderStateWidget(
|
||||||
|
expanded: true,
|
||||||
|
),
|
||||||
|
SimpleEntityState(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePartForPage(BuildContext context) {
|
||||||
|
return SimpleEntityState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
return SliderStateWidget(
|
||||||
|
expanded: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClimateEntity extends Entity {
|
||||||
|
@override
|
||||||
|
double widgetHeight = 38.0;
|
||||||
|
|
||||||
|
static const SUPPORT_TARGET_TEMPERATURE = 1;
|
||||||
|
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
|
||||||
|
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
|
||||||
|
static const SUPPORT_TARGET_HUMIDITY = 8;
|
||||||
|
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
|
||||||
|
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
|
||||||
|
static const SUPPORT_FAN_MODE = 64;
|
||||||
|
static const SUPPORT_OPERATION_MODE = 128;
|
||||||
|
static const SUPPORT_HOLD_MODE = 256;
|
||||||
|
static const SUPPORT_SWING_MODE = 512;
|
||||||
|
static const SUPPORT_AWAY_MODE = 1024;
|
||||||
|
static const SUPPORT_AUX_HEAT = 2048;
|
||||||
|
static const SUPPORT_ON_OFF = 4096;
|
||||||
|
|
||||||
|
bool get supportTargetTemperature => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
|
||||||
|
bool get supportTargetTemperatureHigh => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
|
||||||
|
bool get supportTargetTemperatureLow => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
|
||||||
|
bool get supportTargetHumidity => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
|
||||||
|
bool get supportTargetHumidityHigh => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
|
||||||
|
bool get supportTargetHumidityLow => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
|
||||||
|
bool get supportFanMode =>
|
||||||
|
((attributes["supported_features"] & ClimateEntity.SUPPORT_FAN_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_FAN_MODE);
|
||||||
|
bool get supportOperationMode => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_OPERATION_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_OPERATION_MODE);
|
||||||
|
bool get supportHoldMode =>
|
||||||
|
((attributes["supported_features"] & ClimateEntity.SUPPORT_HOLD_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_HOLD_MODE);
|
||||||
|
bool get supportSwingMode =>
|
||||||
|
((attributes["supported_features"] & ClimateEntity.SUPPORT_SWING_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_SWING_MODE);
|
||||||
|
bool get supportAwayMode =>
|
||||||
|
((attributes["supported_features"] & ClimateEntity.SUPPORT_AWAY_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_AWAY_MODE);
|
||||||
|
bool get supportAuxHeat =>
|
||||||
|
((attributes["supported_features"] & ClimateEntity.SUPPORT_AUX_HEAT) ==
|
||||||
|
ClimateEntity.SUPPORT_AUX_HEAT);
|
||||||
|
bool get supportOnOff =>
|
||||||
|
((attributes["supported_features"] & ClimateEntity.SUPPORT_ON_OFF) ==
|
||||||
|
ClimateEntity.SUPPORT_ON_OFF);
|
||||||
|
|
||||||
|
List<String> get operationList => attributes["operation_list"] != null
|
||||||
|
? (attributes["operation_list"] as List).cast<String>()
|
||||||
|
: null;
|
||||||
|
List<String> get fanList => attributes["fan_list"] != null
|
||||||
|
? (attributes["fan_list"] as List).cast<String>()
|
||||||
|
: null;
|
||||||
|
List<String> get swingList => attributes["swing_list"] != null
|
||||||
|
? (attributes["swing_list"] as List).cast<String>()
|
||||||
|
: null;
|
||||||
|
double get temperature => _getDoubleAttributeValue('temperature');
|
||||||
|
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
|
||||||
|
double get targetLow => _getDoubleAttributeValue('target_temp_low');
|
||||||
|
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
|
||||||
|
double get minTemp => _getDoubleAttributeValue('min_temp') ?? -100.0;
|
||||||
|
double get targetHumidity => _getDoubleAttributeValue('humidity');
|
||||||
|
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
|
||||||
|
double get minHumidity => _getDoubleAttributeValue('min_humidity');
|
||||||
|
String get operationMode => attributes['operation_mode'];
|
||||||
|
String get fanMode => attributes['fan_mode'];
|
||||||
|
String get swingMode => attributes['swing_mode'];
|
||||||
|
bool get awayMode => attributes['away_mode'] == "on";
|
||||||
|
bool get isOff => state == "off";
|
||||||
|
bool get auxHeat => attributes['aux_heat'] == "on";
|
||||||
|
|
||||||
|
ClimateEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return ClimateStateWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
return ClimateControlWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double _getDoubleAttributeValue(String attributeName) {
|
||||||
|
var temp1 = attributes["$attributeName"];
|
||||||
|
if (temp1 is int) {
|
||||||
|
return temp1.toDouble();
|
||||||
|
} else if (temp1 is double) {
|
||||||
|
return temp1;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectEntity extends Entity {
|
||||||
|
List<String> get listOptions => attributes["options"] != null
|
||||||
|
? (attributes["options"] as List).cast<String>()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
SelectEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return SelectControlWidget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DateTimeEntity extends Entity {
|
||||||
|
bool get hasDate => attributes["has_date"] ?? false;
|
||||||
|
bool get hasTime => attributes["has_time"] ?? false;
|
||||||
|
int get year => attributes["year"] ?? 1970;
|
||||||
|
int get month => attributes["month"] ?? 1;
|
||||||
|
int get day => attributes["day"] ?? 1;
|
||||||
|
int get hour => attributes["hour"] ?? 0;
|
||||||
|
int get minute => attributes["minute"] ?? 0;
|
||||||
|
int get second => attributes["second"] ?? 0;
|
||||||
|
String get formattedState => _getFormattedState();
|
||||||
|
DateTime get dateTimeState => _getDateTimeState();
|
||||||
|
|
||||||
|
DateTimeEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return DateTimeStateWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime _getDateTimeState() {
|
||||||
|
return DateTime(
|
||||||
|
this.year, this.month, this.day, this.hour, this.minute, this.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getFormattedState() {
|
||||||
|
String formattedState = "";
|
||||||
|
if (this.hasDate) {
|
||||||
|
formattedState += formatDate(dateTimeState, [M, ' ', d, ', ', yyyy]);
|
||||||
|
}
|
||||||
|
if (this.hasTime) {
|
||||||
|
formattedState += " " + formatDate(dateTimeState, [HH, ':', nn]);
|
||||||
|
}
|
||||||
|
return formattedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setNewState(newValue) {
|
||||||
|
eventBus
|
||||||
|
.fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CoverEntity extends Entity {
|
||||||
|
@override
|
||||||
|
double widgetHeight = 38.0;
|
||||||
|
|
||||||
|
static const SUPPORT_OPEN = 1;
|
||||||
|
static const SUPPORT_CLOSE = 2;
|
||||||
|
static const SUPPORT_SET_POSITION = 4;
|
||||||
|
static const SUPPORT_STOP = 8;
|
||||||
|
static const SUPPORT_OPEN_TILT = 16;
|
||||||
|
static const SUPPORT_CLOSE_TILT = 32;
|
||||||
|
static const SUPPORT_STOP_TILT = 64;
|
||||||
|
static const SUPPORT_SET_TILT_POSITION = 128;
|
||||||
|
|
||||||
|
bool get supportOpen => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_OPEN) ==
|
||||||
|
CoverEntity.SUPPORT_OPEN);
|
||||||
|
bool get supportClose => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_CLOSE) ==
|
||||||
|
CoverEntity.SUPPORT_CLOSE);
|
||||||
|
bool get supportSetPosition => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_SET_POSITION) ==
|
||||||
|
CoverEntity.SUPPORT_SET_POSITION);
|
||||||
|
bool get supportStop => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_STOP) ==
|
||||||
|
CoverEntity.SUPPORT_STOP);
|
||||||
|
|
||||||
|
bool get supportOpenTilt => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_OPEN_TILT) ==
|
||||||
|
CoverEntity.SUPPORT_OPEN_TILT);
|
||||||
|
bool get supportCloseTilt => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_CLOSE_TILT) ==
|
||||||
|
CoverEntity.SUPPORT_CLOSE_TILT);
|
||||||
|
bool get supportStopTilt => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_STOP_TILT) ==
|
||||||
|
CoverEntity.SUPPORT_STOP_TILT);
|
||||||
|
bool get supportSetTiltPosition => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_SET_TILT_POSITION) ==
|
||||||
|
CoverEntity.SUPPORT_SET_TILT_POSITION);
|
||||||
|
|
||||||
|
|
||||||
|
double get currentPosition => _getDoubleAttributeValue('current_position');
|
||||||
|
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position');
|
||||||
|
bool get canBeOpened => ((state != "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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
1029
lib/entity_class/stateful_widgets.dart
Normal file
775
lib/entity_class/stateless_widgets.dart
Normal 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,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
144
lib/entity_collection.class.dart
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class EntityCollection {
|
||||||
|
|
||||||
|
Map<String, Entity> _allEntities;
|
||||||
|
Map<String, Entity> views;
|
||||||
|
|
||||||
|
bool get isEmpty => _allEntities.isEmpty;
|
||||||
|
|
||||||
|
EntityCollection() {
|
||||||
|
_allEntities = {};
|
||||||
|
views = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hasDefaultView => _allEntities["group.default_view"] != null;
|
||||||
|
|
||||||
|
void parse(List rawData) {
|
||||||
|
_allEntities.clear();
|
||||||
|
views.clear();
|
||||||
|
|
||||||
|
TheLogger.log("Debug","Parsing ${rawData.length} Home Assistant entities");
|
||||||
|
rawData.forEach((rawEntityData) {
|
||||||
|
addFromRaw(rawEntityData);
|
||||||
|
});
|
||||||
|
_allEntities.forEach((entityId, entity){
|
||||||
|
if ((entity.isGroup) && (entity.childEntityIds != null)) {
|
||||||
|
entity.childEntities = getAll(entity.childEntityIds);
|
||||||
|
}
|
||||||
|
if (entity.isView) {
|
||||||
|
views[entityId] = entity;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateState(Map rawStateData) {
|
||||||
|
if (isExist(rawStateData["entity_id"])) {
|
||||||
|
updateFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
|
||||||
|
} else {
|
||||||
|
addFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(Entity entity) {
|
||||||
|
_allEntities[entity.entityId] = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity addFromRaw(Map rawEntityData) {
|
||||||
|
Entity entity = _createEntityInstance(rawEntityData);
|
||||||
|
_allEntities[entity.entityId] = entity;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFromRaw(Map rawEntityData) {
|
||||||
|
get("${rawEntityData["entity_id"]}")?.update(rawEntityData);
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity get(String 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) {
|
||||||
|
return _allEntities[entityId] != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String,List<String>> getDefaultViewTopLevelEntities() {
|
||||||
|
Map<String,List<String>> result = {"userGroups": [], "notGroupedEntities": []};
|
||||||
|
List<String> entities = [];
|
||||||
|
_allEntities.forEach((id, entity){
|
||||||
|
if ((id.indexOf("group.") == 0) && (id.indexOf(".all_") == -1) && (!entity.isView)) {
|
||||||
|
result["userGroups"].add(id);
|
||||||
|
}
|
||||||
|
if (!entity.isGroup) {
|
||||||
|
entities.add(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
entities.forEach((entiyId) {
|
||||||
|
bool foundInGroup = false;
|
||||||
|
result["userGroups"].forEach((userGroupId) {
|
||||||
|
if (_allEntities[userGroupId].childEntityIds.contains(entiyId)) {
|
||||||
|
foundInGroup = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!foundInGroup) {
|
||||||
|
result["notGroupedEntities"].add(entiyId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
446
lib/home_assistant.class.dart
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class HomeAssistant {
|
||||||
|
String _webSocketAPIEndpoint;
|
||||||
|
String _password;
|
||||||
|
String _authType;
|
||||||
|
|
||||||
|
IOWebSocketChannel _hassioChannel;
|
||||||
|
SendMessageQueue _messageQueue;
|
||||||
|
|
||||||
|
int _currentMessageId = 0;
|
||||||
|
int _statesMessageId = 0;
|
||||||
|
int _servicesMessageId = 0;
|
||||||
|
int _subscriptionMessageId = 0;
|
||||||
|
int _configMessageId = 0;
|
||||||
|
int _userInfoMessageId = 0;
|
||||||
|
EntityCollection _entities;
|
||||||
|
ViewBuilder _viewBuilder;
|
||||||
|
Map _instanceConfig = {};
|
||||||
|
String _userName;
|
||||||
|
|
||||||
|
Completer _fetchCompleter;
|
||||||
|
Completer _statesCompleter;
|
||||||
|
Completer _servicesCompleter;
|
||||||
|
Completer _configCompleter;
|
||||||
|
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 userName => _userName ?? locationName;
|
||||||
|
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
||||||
|
int get viewsCount => _entities.views.length ?? 0;
|
||||||
|
|
||||||
|
EntityCollection get entities => _entities;
|
||||||
|
|
||||||
|
HomeAssistant() {
|
||||||
|
_entities = EntityCollection();
|
||||||
|
_messageQueue = SendMessageQueue(messageExpirationTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateConnectionSettings(String url, String password, String authType) {
|
||||||
|
_webSocketAPIEndpoint = url;
|
||||||
|
_password = password;
|
||||||
|
_authType = authType;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future fetch() {
|
||||||
|
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
|
||||||
|
TheLogger.log("Warning","Previous fetch is not complited");
|
||||||
|
} else {
|
||||||
|
_fetchCompleter = new Completer();
|
||||||
|
_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();
|
||||||
|
}).catchError((e) {
|
||||||
|
_completeFetching(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return _fetchCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() async {
|
||||||
|
if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) {
|
||||||
|
await _hassioChannel.sink.close().timeout(Duration(seconds: 3),
|
||||||
|
onTimeout: () => TheLogger.log("Debug", "Socket sink closed")
|
||||||
|
);
|
||||||
|
await _socketSubscription.cancel();
|
||||||
|
_hassioChannel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _connection() {
|
||||||
|
if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
|
||||||
|
TheLogger.log("Debug","Previous connection is not complited");
|
||||||
|
} else {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSocketClose() {
|
||||||
|
TheLogger.log("Debug","Socket disconnected. Automatic reconnect is $autoReconnect");
|
||||||
|
if (autoReconnect) {
|
||||||
|
_reconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSocketError(e) {
|
||||||
|
TheLogger.log("Error","Socket stream Error: $e");
|
||||||
|
TheLogger.log("Debug","Automatic reconnect is $autoReconnect");
|
||||||
|
if (autoReconnect) {
|
||||||
|
_reconnect();
|
||||||
|
} else {
|
||||||
|
disconnect().then((_) {
|
||||||
|
_completeConnecting({
|
||||||
|
"errorCode": 1,
|
||||||
|
"errorMessage": "Couldn't connect to Home Assistant. Check network connection or connection settings."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _reconnect() {
|
||||||
|
disconnect().then((_) {
|
||||||
|
_connection().catchError((e){
|
||||||
|
_completeConnecting(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_getData() async {
|
||||||
|
List<Future> futures = [];
|
||||||
|
futures.add(_getStates());
|
||||||
|
futures.add(_getConfig());
|
||||||
|
futures.add(_getServices());
|
||||||
|
futures.add(_getUserInfo());
|
||||||
|
try {
|
||||||
|
await Future.wait(futures);
|
||||||
|
_completeFetching(null);
|
||||||
|
} catch (error) {
|
||||||
|
_completeFetching(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _completeFetching(error) {
|
||||||
|
_fetchTimer.cancel();
|
||||||
|
_completeConnecting(error);
|
||||||
|
if (!_fetchCompleter.isCompleted) {
|
||||||
|
if (error != null) {
|
||||||
|
_fetchCompleter.completeError(error);
|
||||||
|
} else {
|
||||||
|
autoReconnect = true;
|
||||||
|
TheLogger.log("Debug", "Fetch complete successful");
|
||||||
|
_fetchCompleter.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _completeConnecting(error) {
|
||||||
|
_connectionTimer.cancel();
|
||||||
|
if (!_connectionCompleter.isCompleted) {
|
||||||
|
if (error != null) {
|
||||||
|
_connectionCompleter.completeError(error);
|
||||||
|
} else {
|
||||||
|
_connectionCompleter.complete();
|
||||||
|
}
|
||||||
|
} else if (error != null) {
|
||||||
|
eventBus.fire(ShowErrorEvent(error["errorMessage"], error["errorCode"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleMessage(String message) {
|
||||||
|
var data = json.decode(message);
|
||||||
|
if (data["type"] == "auth_required") {
|
||||||
|
_sendAuthMessageRaw('{"type": "auth","$_authType": "$_password"}');
|
||||||
|
} else if (data["type"] == "auth_ok") {
|
||||||
|
_completeConnecting(null);
|
||||||
|
_sendSubscribe();
|
||||||
|
} else if (data["type"] == "auth_invalid") {
|
||||||
|
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
|
||||||
|
} else if (data["type"] == "result") {
|
||||||
|
if (data["id"] == _configMessageId) {
|
||||||
|
_parseConfig(data);
|
||||||
|
} else if (data["id"] == _statesMessageId) {
|
||||||
|
_parseEntities(data);
|
||||||
|
} else if (data["id"] == _servicesMessageId) {
|
||||||
|
_parseServices(data);
|
||||||
|
} else if (data["id"] == _userInfoMessageId) {
|
||||||
|
_parseUserInfo(data);
|
||||||
|
} else if (data["id"] == _currentMessageId) {
|
||||||
|
TheLogger.log("Debug","[Received] => Request id:$_currentMessageId was successful");
|
||||||
|
}
|
||||||
|
} else if (data["type"] == "event") {
|
||||||
|
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"]);
|
||||||
|
} else if (data["event"] != null) {
|
||||||
|
TheLogger.log("Warning","Unhandled event type: ${data["event"]["event_type"]}");
|
||||||
|
} else {
|
||||||
|
TheLogger.log("Error","Event is null: $message");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TheLogger.log("Warning","Unknown message type: $message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sendSubscribe() {
|
||||||
|
_incrementMessageId();
|
||||||
|
_subscriptionMessageId = _currentMessageId;
|
||||||
|
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getConfig() {
|
||||||
|
_configCompleter = new Completer();
|
||||||
|
_incrementMessageId();
|
||||||
|
_configMessageId = _currentMessageId;
|
||||||
|
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}', false);
|
||||||
|
|
||||||
|
return _configCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getStates() {
|
||||||
|
_statesCompleter = new Completer();
|
||||||
|
_incrementMessageId();
|
||||||
|
_statesMessageId = _currentMessageId;
|
||||||
|
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}', false);
|
||||||
|
|
||||||
|
return _statesCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getUserInfo() {
|
||||||
|
_userInfoCompleter = new Completer();
|
||||||
|
_incrementMessageId();
|
||||||
|
_userInfoMessageId = _currentMessageId;
|
||||||
|
_sendMessageRaw('{"id": $_userInfoMessageId, "type": "auth/current_user"}', false);
|
||||||
|
|
||||||
|
return _userInfoCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getServices() {
|
||||||
|
_servicesCompleter = new Completer();
|
||||||
|
_incrementMessageId();
|
||||||
|
_servicesMessageId = _currentMessageId;
|
||||||
|
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}', false);
|
||||||
|
|
||||||
|
return _servicesCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
_incrementMessageId() {
|
||||||
|
_currentMessageId += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sendAuthMessageRaw(String message) {
|
||||||
|
TheLogger.log("Debug", "[Sending] ==> auth request");
|
||||||
|
_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) {
|
||||||
|
//TheLogger.log("Debug", "New state for ${eventData['entity_id']}");
|
||||||
|
Map data = Map.from(eventData);
|
||||||
|
_entities.updateState(data);
|
||||||
|
eventBus.fire(new StateChangedEvent(data["entity_id"], null, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseConfig(Map data) {
|
||||||
|
if (data["success"] == true) {
|
||||||
|
_instanceConfig = Map.from(data["result"]);
|
||||||
|
_configCompleter.complete();
|
||||||
|
} else {
|
||||||
|
_configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseUserInfo(Map data) {
|
||||||
|
if (data["success"] == true) {
|
||||||
|
_userName = data["result"]["name"];
|
||||||
|
} else {
|
||||||
|
_userName = null;
|
||||||
|
}
|
||||||
|
_userInfoCompleter.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseServices(response) {
|
||||||
|
_servicesCompleter.complete();
|
||||||
|
/*if (response["success"] == false) {
|
||||||
|
_servicesCompleter.completeError({"errorCode": 4, "errorMessage": response["error"]["message"]});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Map data = response["result"];
|
||||||
|
Map result = {};
|
||||||
|
TheLogger.log("Debug","Parsing ${data.length} Home Assistant service domains");
|
||||||
|
data.forEach((domain, services) {
|
||||||
|
result[domain] = Map.from(services);
|
||||||
|
services.forEach((serviceName, serviceData) {
|
||||||
|
if (_entitiesData.isExist("$domain.$serviceName")) {
|
||||||
|
result[domain].remove(serviceName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
_servicesData = result;
|
||||||
|
_servicesCompleter.complete();
|
||||||
|
} catch (e) {
|
||||||
|
TheLogger.log("Error","Error parsing services. But they are not used :-)");
|
||||||
|
_servicesCompleter.complete();
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseEntities(response) async {
|
||||||
|
if (response["success"] == false) {
|
||||||
|
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_entities.parse(response["result"]);
|
||||||
|
_viewBuilder = ViewBuilder(entityCollection: _entities);
|
||||||
|
_statesCompleter.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildViews(BuildContext context) {
|
||||||
|
return _viewBuilder.buildWidget(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List> getHistory(String entityId) async {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||||
|
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||||
|
TheLogger.log("Debug", "$startTime");
|
||||||
|
String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId&skip_initial_state";
|
||||||
|
TheLogger.log("Debug", "$url");
|
||||||
|
http.Response historyResponse;
|
||||||
|
if (_authType == "access_token") {
|
||||||
|
historyResponse = await http.get(url, headers: {
|
||||||
|
"authorization": "Bearer $_password",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
historyResponse = await http.get(url, headers: {
|
||||||
|
"X-HA-Access": "$_password",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var _history = json.decode(historyResponse.body);
|
||||||
|
if (_history is Map) {
|
||||||
|
return null;
|
||||||
|
} else if (_history is List) {
|
||||||
|
TheLogger.log("Debug", "${_history[0].toString()}");
|
||||||
|
return _history;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SendMessageQueue {
|
||||||
|
int _messageTimeout;
|
||||||
|
List<HAMessage> _queue = [];
|
||||||
|
|
||||||
|
SendMessageQueue(this._messageTimeout);
|
||||||
|
|
||||||
|
void add(String message) {
|
||||||
|
_queue.add(HAMessage(_messageTimeout, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> getActualMessages() {
|
||||||
|
_queue.removeWhere((item) => item.isExpired());
|
||||||
|
List<String> result = [];
|
||||||
|
_queue.forEach((haMessage){
|
||||||
|
result.add(haMessage.message);
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -10,11 +10,7 @@ class LogViewPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LogViewPageState extends State<LogViewPage> {
|
class _LogViewPageState extends State<LogViewPage> {
|
||||||
String _hassioDomain = "";
|
String _logData;
|
||||||
String _hassioPort = "8123";
|
|
||||||
String _hassioPassword = "";
|
|
||||||
String _socketProtocol = "wss";
|
|
||||||
String _authType = "access_token";
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -23,7 +19,7 @@ class _LogViewPageState extends State<LogViewPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_loadLog() async {
|
_loadLog() async {
|
||||||
//
|
_logData = TheLogger.getLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -36,12 +32,28 @@ class _LogViewPageState extends State<LogViewPage> {
|
|||||||
// Here we take the value from the MyHomePage object that was created by
|
// Here we take the value from the MyHomePage object that was created by
|
||||||
// the App.build method, and use it to set our appbar title.
|
// the App.build method, and use it to set our appbar title.
|
||||||
title: new Text(widget.title),
|
title: new Text(widget.title),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.content_copy),
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(new ClipboardData(text: _logData));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")),
|
||||||
|
onPressed: () {
|
||||||
|
String body = "```\n$_logData```";
|
||||||
|
String encodedBody = "${Uri.encodeFull(body)}";
|
||||||
|
HAUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new?body=$encodedBody");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: TextField(
|
body: TextField(
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
|
|
||||||
controller: TextEditingController(
|
controller: TextEditingController(
|
||||||
text: TheLogger.getLog()
|
text: _logData
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
685
lib/main.dart
@ -8,50 +8,52 @@ import 'package:progress_indicators/progress_indicators.dart';
|
|||||||
import 'package:event_bus/event_bus.dart';
|
import 'package:event_bus/event_bus.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:date_format/date_format.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:flutter_colorpicker/material_picker.dart';
|
||||||
|
|
||||||
part 'settingsPage.dart';
|
part 'entity_class/entity.class.dart';
|
||||||
part 'data_model.dart';
|
part 'entity_class/stateless_widgets.dart';
|
||||||
part 'logPage.dart';
|
part 'entity_class/stateful_widgets.dart';
|
||||||
|
|
||||||
|
part 'settings.page.dart';
|
||||||
|
part 'home_assistant.class.dart';
|
||||||
|
part 'log.page.dart';
|
||||||
|
part 'entity.page.dart';
|
||||||
|
part 'utils.class.dart';
|
||||||
|
part 'mdi.class.dart';
|
||||||
|
part 'entity_collection.class.dart';
|
||||||
|
part 'view_builder.class.dart';
|
||||||
|
part 'view_class.dart';
|
||||||
|
part 'card_class.dart';
|
||||||
|
|
||||||
EventBus eventBus = new EventBus();
|
EventBus eventBus = new EventBus();
|
||||||
const String appName = "HA Client";
|
const String appName = "HA Client";
|
||||||
const appVersion = "0.1.1-alpha";
|
const appVersion = "0.3.3";
|
||||||
|
|
||||||
String homeAssistantWebHost;
|
String homeAssistantWebHost;
|
||||||
|
|
||||||
class TheLogger {
|
void main() {
|
||||||
|
FlutterError.onError = (errorDetails) {
|
||||||
|
TheLogger.log("Error", "${errorDetails.exception}");
|
||||||
|
if (TheLogger.isInDebugMode) {
|
||||||
|
FlutterError.dumpErrorToConsole(errorDetails);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
static List<String> _log = [];
|
runZoned(() {
|
||||||
|
runApp(new HAClientApp());
|
||||||
static String getLog() {
|
}, onError: (error, stack) {
|
||||||
String res = '';
|
TheLogger.log("Global error", "$error");
|
||||||
_log.forEach((line) {
|
if (TheLogger.isInDebugMode) {
|
||||||
res += "$line\n\n";
|
debugPrint("$stack");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool get isInDebugMode {
|
class HAClientApp extends StatelessWidget {
|
||||||
bool inDebugMode = false;
|
|
||||||
|
|
||||||
assert(inDebugMode = true);
|
|
||||||
|
|
||||||
return inDebugMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void log(String level, String message) {
|
|
||||||
debugPrint('$message');
|
|
||||||
_log.add("[$level] : $message");
|
|
||||||
if (_log.length > 50) {
|
|
||||||
_log.removeAt(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() => runApp(new HassClientApp());
|
|
||||||
|
|
||||||
class HassClientApp extends StatelessWidget {
|
|
||||||
// This widget is the root of your application.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -62,7 +64,7 @@ class HassClientApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
initialRoute: "/",
|
initialRoute: "/",
|
||||||
routes: {
|
routes: {
|
||||||
"/": (context) => MainPage(title: 'Hass Client'),
|
"/": (context) => MainPage(title: 'HA Client'),
|
||||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Connection Settings"),
|
"/connection-settings": (context) => ConnectionSettingsPage(title: "Connection Settings"),
|
||||||
"/log-view": (context) => LogViewPage(title: "Log")
|
"/log-view": (context) => LogViewPage(title: "Log")
|
||||||
},
|
},
|
||||||
@ -80,401 +82,185 @@ class MainPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||||
HassioDataModel _dataModel;
|
HomeAssistant _homeAssistant;
|
||||||
Map _entitiesData;
|
EntityCollection _entities;
|
||||||
Map _uiStructure;
|
//Map _instanceConfig;
|
||||||
Map _instanceConfig;
|
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;
|
||||||
bool _isLoading = true;
|
StreamSubscription _serviceCallSubscription;
|
||||||
Map<String, Color> _stateIconColors = {
|
StreamSubscription _showEntityPageSubscription;
|
||||||
"on": Colors.amber,
|
StreamSubscription _refreshDataSubscription;
|
||||||
"off": Color.fromRGBO(68, 115, 158, 1.0),
|
StreamSubscription _showErrorSubscription;
|
||||||
"unavailable": Colors.black12,
|
int _isLoading = 1;
|
||||||
"unknown": Colors.black12,
|
bool _settingsLoaded = false;
|
||||||
"playing": Colors.amber
|
bool _accountMenuExpanded = false;
|
||||||
};
|
|
||||||
Map<String, Color> _badgeColors = {
|
|
||||||
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
|
||||||
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
|
||||||
};
|
|
||||||
|
|
||||||
@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 (_dataModel != null) _dataModel.closeConnection();
|
_settingsLoaded = true;
|
||||||
_createConnection(apiEndpoint, apiPassword, authType);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_createConnection(String apiEndpoint, String apiPassword, String authType) {
|
_subscribe() {
|
||||||
_dataModel = HassioDataModel(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(() {
|
||||||
_entitiesData = _dataModel.entities;
|
if (event.localChange) {
|
||||||
|
_entities
|
||||||
|
.get(event.entityId)
|
||||||
|
.state = event.newState;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (_serviceCallSubscription == null) {
|
||||||
|
_serviceCallSubscription =
|
||||||
|
eventBus.on<ServiceCallEvent>().listen((event) {
|
||||||
|
_callService(event.domain, event.service, event.entityId,
|
||||||
|
event.additionalParams);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_showEntityPageSubscription == null) {
|
||||||
|
_showEntityPageSubscription =
|
||||||
|
eventBus.on<ShowEntityPageEvent>().listen((event) {
|
||||||
|
_showEntityPage(event.entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_refreshDataSubscription == null) {
|
||||||
|
_refreshDataSubscription = eventBus.on<RefreshDataEvent>().listen((event){
|
||||||
|
_refreshData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_showErrorSubscription == null) {
|
||||||
|
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
|
||||||
|
_showErrorSnackBar(message: event.text, errorCode: event.errorCode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_refreshData() async {
|
_refreshData() async {
|
||||||
|
_homeAssistant.updateConnectionSettings(_webSocketApiEndpoint, _password, _authType);
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_hideErrorSnackBar();
|
||||||
|
_isLoading = 1;
|
||||||
});
|
});
|
||||||
_errorCodeToBeShown = 0;
|
await _homeAssistant.fetch().then((result) {
|
||||||
if (_dataModel != null) {
|
|
||||||
await _dataModel.fetch().then((result) {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_instanceConfig = _dataModel.instanceConfig;
|
//_instanceConfig = _homeAssistant.instanceConfig;
|
||||||
_entitiesData = _dataModel.entities;
|
_entities = _homeAssistant.entities;
|
||||||
_uiStructure = _dataModel.uiStructure;
|
_uiViewsCount = _homeAssistant.viewsCount;
|
||||||
_uiViewsCount = _uiStructure.length;
|
TheLogger.log("Debug","_uiViewsCount=$_uiViewsCount");
|
||||||
_isLoading = false;
|
_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) {
|
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;
|
|
||||||
});
|
|
||||||
_dataModel.callService(domain, service, entityId).then((r) {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
}).catchError((e) => _setErrorState(e));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildViews() {
|
void _showEntityPage(Entity entity) {
|
||||||
List<Widget> result = [];
|
Navigator.push(
|
||||||
if ((_entitiesData != null) && (_uiStructure != null)) {
|
context,
|
||||||
_uiStructure.forEach((viewId, structure) {
|
MaterialPageRoute(
|
||||||
result.add(
|
builder: (context) => EntityViewPage(entity: entity, homeAssistant: _homeAssistant),
|
||||||
RefreshIndicator(
|
|
||||||
color: Colors.amber,
|
|
||||||
child: ListView(
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
children: _buildSingleView(structure),
|
|
||||||
),
|
|
||||||
onRefresh: () => _refreshData(),
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _buildSingleView(structure) {
|
|
||||||
List<Widget> result = [];
|
|
||||||
if (structure["badges"]["children"].length > 0) {
|
|
||||||
result.add(
|
|
||||||
Wrap(
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
spacing: 10.0,
|
|
||||||
runSpacing: 4.0,
|
|
||||||
//padding: new EdgeInsets.all(8.0),
|
|
||||||
//itemExtent: 40.0,
|
|
||||||
children: _buildBadges(structure["badges"]["children"]),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
structure["groups"].forEach((id, group) {
|
|
||||||
if (group["children"].length > 0) {
|
|
||||||
result.add(_buildCard(
|
|
||||||
group["children"], group["friendly_name"].toString()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _buildBadges(List ids) {
|
|
||||||
List<Widget> result = [];
|
|
||||||
ids.forEach((entityId) {
|
|
||||||
var data = _entitiesData[entityId];
|
|
||||||
if (data != null) {
|
|
||||||
result.add(
|
|
||||||
_buildSingleBadge(data)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSingleBadge(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["attributes"]["unit_of_measurement"];
|
|
||||||
badgeIcon = Center(
|
|
||||||
child: Text(
|
|
||||||
"${data['state']}",
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
softWrap: false,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 18.0),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "device_tracker": {
|
|
||||||
badgeIcon = MaterialDesignIcons.createIconFromEntityData(data, iconSize,Colors.black);
|
|
||||||
badgeTextValue = data["state"];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
badgeIcon = MaterialDesignIcons.createIconFromEntityData(data, iconSize,Colors.black);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Widget badgeText;
|
|
||||||
if (badgeTextValue == null) {
|
|
||||||
badgeText = Container(width: 0.0, height: 0.0);
|
|
||||||
} else {
|
|
||||||
badgeText = Container(
|
|
||||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
|
||||||
child: Text("$badgeTextValue",
|
|
||||||
style: TextStyle(fontSize: 13.0, color: Colors.white),
|
|
||||||
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: -15.0,
|
|
||||||
right: -15.0,
|
|
||||||
child: Center(
|
|
||||||
child: badgeText,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 60.0,
|
|
||||||
child: Text(
|
|
||||||
"${data['display_name']}",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
softWrap: true,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Card _buildCard(List ids, String name) {
|
|
||||||
List<Widget> body = [];
|
|
||||||
body.add(_buildCardHeader(name));
|
|
||||||
body.addAll(_buildCardBody(ids));
|
|
||||||
Card result =
|
|
||||||
Card(child: new Column(mainAxisSize: MainAxisSize.min, children: body));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCardHeader(String name) {
|
|
||||||
var result;
|
|
||||||
if (name.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 data = _entitiesData[id];
|
|
||||||
if (data != null) {
|
|
||||||
entities.add(new ListTile(
|
|
||||||
leading: MaterialDesignIcons.createIconFromEntityData(data, 28.0, _stateIconColors[data["state"]] ?? Colors.blueGrey),
|
|
||||||
//subtitle: Text("${data['entity_id']}"),
|
|
||||||
trailing: _buildEntityActionWidget(data),
|
|
||||||
title: Text(
|
|
||||||
"${data["display_name"]}",
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
softWrap: false,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return entities;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEntityActionWidget(data) {
|
|
||||||
String entityId = data["entity_id"];
|
|
||||||
Widget result;
|
|
||||||
switch (data["domain"]) {
|
|
||||||
case "automation":
|
|
||||||
case "switch":
|
|
||||||
case "light": {
|
|
||||||
result = Switch(
|
|
||||||
value: (data["state"] == "on"),
|
|
||||||
onChanged: ((state) {
|
|
||||||
_callService(
|
|
||||||
data["domain"], state ? "turn_on" : "turn_off", entityId);
|
|
||||||
setState(() {
|
|
||||||
_entitiesData[entityId]["state"] = state ? "on" : "off";
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "script":
|
|
||||||
case "scene": {
|
|
||||||
result = SizedBox(
|
|
||||||
width: 60.0,
|
|
||||||
child: FlatButton(
|
|
||||||
onPressed: (() {
|
|
||||||
_callService(data["domain"], "turn_on", entityId);
|
|
||||||
}),
|
|
||||||
child: Text(
|
|
||||||
"Run",
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
style: new TextStyle(fontSize: 16.0, color: Colors.blue),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
result = Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(0.0, 0.0, 16.0, 0.0),
|
|
||||||
child: Text(
|
|
||||||
"${data["state"]}${(data["attributes"] != null && data["attributes"]["unit_of_measurement"] != null) ? data["attributes"]["unit_of_measurement"] : ''}",
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
style: new TextStyle(
|
|
||||||
fontSize: 16.0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*return SizedBox(
|
|
||||||
width: 60.0,
|
|
||||||
// height: double.infinity,
|
|
||||||
child: result
|
|
||||||
);*/
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Tab> buildUIViewTabs() {
|
List<Tab> buildUIViewTabs() {
|
||||||
List<Tab> result = [];
|
List<Tab> result = [];
|
||||||
if ((_entitiesData != null) && (_uiStructure != null)) {
|
if (!_entities.isEmpty) {
|
||||||
_uiStructure.forEach((viewId, structure) {
|
if (!_entities.hasDefaultView) {
|
||||||
result.add(
|
result.add(
|
||||||
Tab(
|
Tab(
|
||||||
icon: MaterialDesignIcons.createIconFromEntityData(structure, 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(
|
||||||
|
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
|
||||||
|
size: 24.0,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -484,9 +270,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
Widget _buildAppTitle() {
|
Widget _buildAppTitle() {
|
||||||
Row titleRow = Row(
|
Row titleRow = Row(
|
||||||
children: [Text(_instanceConfig != null ? _instanceConfig["location_name"] : "")],
|
children: [Text(_homeAssistant != null ? _homeAssistant.locationName : "")],
|
||||||
);
|
);
|
||||||
if (_isLoading) {
|
if (_isLoading == 1) {
|
||||||
titleRow.children.add(Padding(
|
titleRow.children.add(Padding(
|
||||||
child: JumpingDotsProgressIndicator(
|
child: JumpingDotsProgressIndicator(
|
||||||
fontSize: 26.0,
|
fontSize: 26.0,
|
||||||
@ -494,48 +280,95 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 30.0),
|
padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 30.0),
|
||||||
));
|
));
|
||||||
|
} else if (_isLoading == 2) {
|
||||||
|
titleRow.children.add(Padding(
|
||||||
|
child: Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 20.0,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 0.0),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
return titleRow;
|
return titleRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
Drawer _buildAppDrawer() {
|
Drawer _buildAppDrawer() {
|
||||||
return new Drawer(
|
List<Widget> menuItems = [];
|
||||||
child: ListView(
|
menuItems.add(
|
||||||
children: <Widget>[
|
UserAccountsDrawerHeader(
|
||||||
new UserAccountsDrawerHeader(
|
accountName: Text(_homeAssistant.userName),
|
||||||
accountName: Text(_instanceConfig != null ? _instanceConfig["location_name"] : "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: () {
|
||||||
Navigator.pushNamed(context, '/connection-settings');
|
Navigator.of(context).pop();
|
||||||
|
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"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pushNamed(context, '/log-view');
|
Navigator.of(context).pop();
|
||||||
|
Navigator.of(context).pushNamed('/log-view');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
new ListTile(
|
||||||
|
leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")),
|
||||||
|
title: Text("Report an issue"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
HAUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
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",
|
||||||
@ -570,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();
|
||||||
@ -592,79 +425,65 @@ 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>();
|
||||||
|
|
||||||
@override
|
Scaffold _buildScaffold(bool empty) {
|
||||||
Widget build(BuildContext context) {
|
return Scaffold(
|
||||||
_checkShowInfo(context);
|
|
||||||
// This method is rerun every time setState is called.
|
|
||||||
//
|
|
||||||
if (_entitiesData == null) {
|
|
||||||
return new Scaffold(
|
|
||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
appBar: new AppBar(
|
appBar: AppBar(
|
||||||
title: _buildAppTitle()
|
title: _buildAppTitle(),
|
||||||
|
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: Center(
|
body: empty ?
|
||||||
|
Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
/*Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 10.0),
|
|
||||||
child: Text(
|
|
||||||
_fetchErrorCode > 0 ? "Well... no.\n\nThere was an error [$_fetchErrorCode]: ${_getErrorMessageByCode(_fetchErrorCode, false)}" : "Loading...",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 16.0),
|
|
||||||
),
|
|
||||||
),*/
|
|
||||||
Icon(
|
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,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
floatingActionButton: new FloatingActionButton(
|
:
|
||||||
onPressed: _refreshData,
|
_homeAssistant.buildViews(context)
|
||||||
tooltip: 'Increment',
|
|
||||||
child: new Icon(Icons.refresh),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// This method is rerun every time setState is called.
|
||||||
|
if (_entities == null) {
|
||||||
|
return _buildScaffold(true);
|
||||||
} else {
|
} else {
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: _uiViewsCount,
|
length: _uiViewsCount,
|
||||||
child: new Scaffold(
|
child: _buildScaffold(false)
|
||||||
key: _scaffoldKey,
|
|
||||||
appBar: new AppBar(
|
|
||||||
// Here we take the value from the MyHomePage object that was created by
|
|
||||||
// the App.build method, and use it to set our appbar title.
|
|
||||||
title: _buildAppTitle(),
|
|
||||||
bottom: TabBar(
|
|
||||||
tabs: buildUIViewTabs()
|
|
||||||
),
|
|
||||||
),
|
|
||||||
drawer: _buildAppDrawer(),
|
|
||||||
body: TabBarView(
|
|
||||||
children: _buildViews()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -674,7 +493,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
if (_stateSubscription != null) _stateSubscription.cancel();
|
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||||
if (_settingsSubscription != null) _settingsSubscription.cancel();
|
if (_settingsSubscription != null) _settingsSubscription.cancel();
|
||||||
_dataModel.closeConnection();
|
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
|
||||||
|
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
|
||||||
|
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
|
||||||
|
if (_showErrorSubscription != null) _showErrorSubscription.cancel();
|
||||||
|
_homeAssistant.disconnect();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,362 +1,5 @@
|
|||||||
part of 'main.dart';
|
part of 'main.dart';
|
||||||
|
|
||||||
class StateChangedEvent {
|
|
||||||
String entityId;
|
|
||||||
|
|
||||||
StateChangedEvent(this.entityId);
|
|
||||||
}
|
|
||||||
|
|
||||||
class SettingsChangedEvent {
|
|
||||||
bool reconnect;
|
|
||||||
|
|
||||||
SettingsChangedEvent(this.reconnect);
|
|
||||||
}
|
|
||||||
|
|
||||||
class HassioDataModel {
|
|
||||||
String _hassioAPIEndpoint;
|
|
||||||
String _hassioPassword;
|
|
||||||
String _hassioAuthType;
|
|
||||||
IOWebSocketChannel _hassioChannel;
|
|
||||||
int _currentMessageId = 0;
|
|
||||||
int _statesMessageId = 0;
|
|
||||||
int _servicesMessageId = 0;
|
|
||||||
int _subscriptionMessageId = 0;
|
|
||||||
int _configMessageId = 0;
|
|
||||||
Map _entitiesData = {};
|
|
||||||
Map _servicesData = {};
|
|
||||||
Map _uiStructure = {};
|
|
||||||
Map _instanceConfig = {};
|
|
||||||
Completer _fetchCompleter;
|
|
||||||
Completer _statesCompleter;
|
|
||||||
Completer _servicesCompleter;
|
|
||||||
Completer _configCompleter;
|
|
||||||
Timer _fetchingTimer;
|
|
||||||
List _topBadgeDomains = ["alarm_control_panel", "binary_sensor", "device_tracker", "updater", "sun", "timer", "sensor"];
|
|
||||||
|
|
||||||
Map get entities => _entitiesData;
|
|
||||||
Map get services => _servicesData;
|
|
||||||
Map get uiStructure => _uiStructure;
|
|
||||||
Map get instanceConfig => _instanceConfig;
|
|
||||||
|
|
||||||
HassioDataModel(String url, String password, String authType) {
|
|
||||||
_hassioAPIEndpoint = url;
|
|
||||||
_hassioPassword = password;
|
|
||||||
_hassioAuthType = authType;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future fetch() {
|
|
||||||
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
|
|
||||||
TheLogger.log("Warning","Previous fetch is not complited");
|
|
||||||
} else {
|
|
||||||
//TODO: Fetch timeout timer. Should be removed after #21 fix
|
|
||||||
_fetchingTimer = Timer(Duration(seconds: 15), () {
|
|
||||||
closeConnection();
|
|
||||||
_fetchCompleter.completeError({"errorCode" : 1,"errorMessage": "Connection timeout"});
|
|
||||||
});
|
|
||||||
_fetchCompleter = new Completer();
|
|
||||||
_reConnectSocket().then((r) {
|
|
||||||
_getData();
|
|
||||||
}).catchError((e) {
|
|
||||||
_finishFetching(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return _fetchCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
closeConnection() {
|
|
||||||
if (_hassioChannel?.closeCode == null) {
|
|
||||||
_hassioChannel?.sink?.close();
|
|
||||||
}
|
|
||||||
_hassioChannel = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _reConnectSocket() {
|
|
||||||
var _connectionCompleter = new Completer();
|
|
||||||
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
|
|
||||||
TheLogger.log("Debug","Socket connecting...");
|
|
||||||
_hassioChannel = IOWebSocketChannel.connect(_hassioAPIEndpoint);
|
|
||||||
_hassioChannel.stream.handleError((e) {
|
|
||||||
TheLogger.log("Error","Unhandled socket error: ${e.toString()}");
|
|
||||||
});
|
|
||||||
_hassioChannel.stream.listen((message) =>
|
|
||||||
_handleMessage(_connectionCompleter, message));
|
|
||||||
} else {
|
|
||||||
_connectionCompleter.complete();
|
|
||||||
}
|
|
||||||
return _connectionCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getData() {
|
|
||||||
_getConfig().then((result) {
|
|
||||||
_getStates().then((result) {
|
|
||||||
_getServices().then((result) {
|
|
||||||
_finishFetching(null);
|
|
||||||
}).catchError((e) {
|
|
||||||
_finishFetching(e);
|
|
||||||
});
|
|
||||||
}).catchError((e) {
|
|
||||||
_finishFetching(e);
|
|
||||||
});
|
|
||||||
}).catchError((e) {
|
|
||||||
_finishFetching(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_finishFetching(error) {
|
|
||||||
_fetchingTimer.cancel();
|
|
||||||
if (error != null) {
|
|
||||||
_fetchCompleter.completeError(error);
|
|
||||||
} else {
|
|
||||||
_fetchCompleter.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleMessage(Completer connectionCompleter, String message) {
|
|
||||||
var data = json.decode(message);
|
|
||||||
TheLogger.log("Debug","[Received] => Message type: ${data['type']}");
|
|
||||||
if (data["type"] == "auth_required") {
|
|
||||||
_sendMessageRaw('{"type": "auth","$_hassioAuthType": "$_hassioPassword"}');
|
|
||||||
} else if (data["type"] == "auth_ok") {
|
|
||||||
_sendSubscribe();
|
|
||||||
connectionCompleter.complete();
|
|
||||||
} else if (data["type"] == "auth_invalid") {
|
|
||||||
connectionCompleter.completeError({"errorCode": 6, "errorMessage": "${data["message"]}"});
|
|
||||||
} else if (data["type"] == "result") {
|
|
||||||
if (data["id"] == _configMessageId) {
|
|
||||||
_parseConfig(data);
|
|
||||||
} else if (data["id"] == _statesMessageId) {
|
|
||||||
_parseEntities(data);
|
|
||||||
} else if (data["id"] == _servicesMessageId) {
|
|
||||||
_parseServices(data);
|
|
||||||
} else if (data["id"] == _currentMessageId) {
|
|
||||||
TheLogger.log("Debug","Request id:$_currentMessageId was successful");
|
|
||||||
}
|
|
||||||
} else if (data["type"] == "event") {
|
|
||||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
|
||||||
_handleEntityStateChange(data["event"]["data"]);
|
|
||||||
} else if (data["event"] != null) {
|
|
||||||
TheLogger.log("Warning","Unhandled event type: ${data["event"]["event_type"]}");
|
|
||||||
} else {
|
|
||||||
TheLogger.log("Error","Event is null: $message");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TheLogger.log("Warning","Unknown message type: $message");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sendSubscribe() {
|
|
||||||
_incrementMessageId();
|
|
||||||
_subscriptionMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getConfig() {
|
|
||||||
_configCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_configMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}');
|
|
||||||
|
|
||||||
return _configCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getStates() {
|
|
||||||
_statesCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_statesMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}');
|
|
||||||
|
|
||||||
return _statesCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getServices() {
|
|
||||||
_servicesCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_servicesMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}');
|
|
||||||
|
|
||||||
return _servicesCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
_incrementMessageId() {
|
|
||||||
_currentMessageId += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_sendMessageRaw(String message) {
|
|
||||||
if (message.indexOf('"type": "auth"') > 0) {
|
|
||||||
TheLogger.log("Debug", "[Sending] ==> auth request");
|
|
||||||
} else {
|
|
||||||
TheLogger.log("Debug", "[Sending] ==> $message");
|
|
||||||
}
|
|
||||||
_hassioChannel.sink.add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleEntityStateChange(Map eventData) {
|
|
||||||
TheLogger.log("Debug", "Parsing new state for ${eventData['entity_id']}");
|
|
||||||
if (eventData["new_state"] == null) {
|
|
||||||
TheLogger.log("Error", "No new_state found");
|
|
||||||
} else {
|
|
||||||
var parsedEntityData = _parseEntity(eventData["new_state"]);
|
|
||||||
String entityId = parsedEntityData["entity_id"];
|
|
||||||
if (_entitiesData[entityId] == null) {
|
|
||||||
_entitiesData[entityId] = parsedEntityData;
|
|
||||||
} else {
|
|
||||||
_entitiesData[entityId].addAll(parsedEntityData);
|
|
||||||
}
|
|
||||||
eventBus.fire(new StateChangedEvent(eventData["entity_id"]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseConfig(Map data) {
|
|
||||||
if (data["success"] == true) {
|
|
||||||
_instanceConfig = Map.from(data["result"]);
|
|
||||||
_configCompleter.complete();
|
|
||||||
} else {
|
|
||||||
_configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseServices(response) {
|
|
||||||
if (response["success"] == false) {
|
|
||||||
_servicesCompleter.completeError({"errorCode": 4, "errorMessage": response["error"]["message"]});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Map data = response["result"];
|
|
||||||
Map result = {};
|
|
||||||
TheLogger.log("Debug","Parsing ${data.length} Home Assistant service domains");
|
|
||||||
data.forEach((domain, services) {
|
|
||||||
result[domain] = Map.from(services);
|
|
||||||
services.forEach((serviceName, serviceData) {
|
|
||||||
if (_entitiesData["$domain.$serviceName"] != null) {
|
|
||||||
result[domain].remove(serviceName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
_servicesData = result;
|
|
||||||
_servicesCompleter.complete();
|
|
||||||
} catch (e) {
|
|
||||||
//TODO hadle it properly
|
|
||||||
TheLogger.log("Error","Error parsing services. But they are not used :-)");
|
|
||||||
_servicesCompleter.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseEntities(response) async {
|
|
||||||
_entitiesData.clear();
|
|
||||||
_uiStructure.clear();
|
|
||||||
if (response["success"] == false) {
|
|
||||||
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
List data = response["result"];
|
|
||||||
TheLogger.log("Debug","Parsing ${data.length} Home Assistant entities");
|
|
||||||
List<String> uiGroups = [];
|
|
||||||
data.forEach((entity) {
|
|
||||||
try {
|
|
||||||
var composedEntity = _parseEntity(entity);
|
|
||||||
|
|
||||||
if (composedEntity["attributes"] != null) {
|
|
||||||
if ((composedEntity["domain"] == "group") &&
|
|
||||||
(composedEntity["attributes"]["view"] == true)) {
|
|
||||||
uiGroups.add(composedEntity["entity_id"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_entitiesData[entity["entity_id"]] = composedEntity;
|
|
||||||
} catch (error) {
|
|
||||||
TheLogger.log("Error","Error parsing entity: ${entity['entity_id']}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Gethering information for UI
|
|
||||||
TheLogger.log("Debug","Gethering views");
|
|
||||||
int viewCounter = 0;
|
|
||||||
uiGroups.forEach((viewId) { //Each view
|
|
||||||
try {
|
|
||||||
Map viewGroupStructure = {};
|
|
||||||
viewCounter += 1;
|
|
||||||
var viewGroup = _entitiesData[viewId];
|
|
||||||
if (viewGroup != null) {
|
|
||||||
viewGroupStructure["groups"] = {};
|
|
||||||
viewGroupStructure["state"] = "on";
|
|
||||||
viewGroupStructure["entity_id"] = viewGroup["entity_id"];
|
|
||||||
viewGroupStructure["badges"] = {"children": []};
|
|
||||||
viewGroupStructure["attributes"] = viewGroup["attributes"] != null ? {
|
|
||||||
"icon": viewGroup["attributes"]["icon"]
|
|
||||||
} : {"icon": "none"};
|
|
||||||
|
|
||||||
|
|
||||||
viewGroup["attributes"]["entity_id"].forEach((
|
|
||||||
entityId) { //Each entity or group in view
|
|
||||||
Map newGroup = {};
|
|
||||||
String domain = _entitiesData[entityId]["domain"];
|
|
||||||
if (domain != "group") {
|
|
||||||
if (_topBadgeDomains.contains(domain)) {
|
|
||||||
viewGroupStructure["badges"]["children"].add(entityId);
|
|
||||||
} else {
|
|
||||||
String autoGroupID = "$domain.$domain$viewCounter";
|
|
||||||
if (viewGroupStructure["groups"]["$autoGroupID"] == null) {
|
|
||||||
newGroup["entity_id"] = "$domain.$domain$viewCounter";
|
|
||||||
newGroup["friendly_name"] = "$domain";
|
|
||||||
newGroup["children"] = [];
|
|
||||||
newGroup["children"].add(entityId);
|
|
||||||
viewGroupStructure["groups"]["$autoGroupID"] =
|
|
||||||
Map.from(newGroup);
|
|
||||||
} else {
|
|
||||||
viewGroupStructure["groups"]["$autoGroupID"]["children"].add(
|
|
||||||
entityId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newGroup["entity_id"] = entityId;
|
|
||||||
newGroup["friendly_name"] =
|
|
||||||
(_entitiesData[entityId]['attributes'] != null)
|
|
||||||
? (_entitiesData[entityId]['attributes']['friendly_name'] ??
|
|
||||||
"")
|
|
||||||
: "";
|
|
||||||
newGroup["children"] = List<String>();
|
|
||||||
_entitiesData[entityId]["attributes"]["entity_id"].forEach((
|
|
||||||
groupedEntityId) {
|
|
||||||
newGroup["children"].add(groupedEntityId);
|
|
||||||
});
|
|
||||||
viewGroupStructure["groups"]["$entityId"] = Map.from(newGroup);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_uiStructure[viewId.split(".")[1]] = viewGroupStructure;
|
|
||||||
} catch (error) {
|
|
||||||
TheLogger.log("Error","Error parsing view: $viewId");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_statesCompleter.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map _parseEntity(rawData) {
|
|
||||||
var composedEntity = Map.from(rawData);
|
|
||||||
String entityDomain = rawData["entity_id"].split(".")[0];
|
|
||||||
composedEntity["display_name"] = "${rawData["attributes"]!=null ? rawData["attributes"]["friendly_name"] ?? rawData["attributes"]["name"] : "_"}";
|
|
||||||
composedEntity["domain"] = entityDomain;
|
|
||||||
return composedEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future callService(String domain, String service, String entity_id) {
|
|
||||||
var sendCompleter = Completer();
|
|
||||||
//TODO: Send service call timeout timer. Should be removed after #21 fix
|
|
||||||
Timer _sendTimer = Timer(Duration(seconds: 7), () {
|
|
||||||
sendCompleter.completeError({"errorCode" : 8,"errorMessage": "Connection timeout"});
|
|
||||||
});
|
|
||||||
_reConnectSocket().then((r) {
|
|
||||||
_incrementMessageId();
|
|
||||||
_sendMessageRaw('{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entity_id"}}');
|
|
||||||
_sendTimer.cancel();
|
|
||||||
sendCompleter.complete();
|
|
||||||
}).catchError((e){
|
|
||||||
_sendTimer.cancel();
|
|
||||||
sendCompleter.completeError(e);
|
|
||||||
});
|
|
||||||
return sendCompleter.future;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MaterialDesignIcons {
|
class MaterialDesignIcons {
|
||||||
static Map _defaultIconsByDomains = {
|
static Map _defaultIconsByDomains = {
|
||||||
"light": "mdi:lightbulb",
|
"light": "mdi:lightbulb",
|
||||||
@ -371,7 +14,14 @@ class MaterialDesignIcons {
|
|||||||
"input_number": "mdi:ray-vertex",
|
"input_number": "mdi:ray-vertex",
|
||||||
"input_select": "mdi:format-list-bulleted",
|
"input_select": "mdi:format-list-bulleted",
|
||||||
"input_text": "mdi:textbox",
|
"input_text": "mdi:textbox",
|
||||||
"sun": "mdi:white-balance-sunny"
|
"sun": "mdi:white-balance-sunny",
|
||||||
|
"scene": "mdi:google-pages",
|
||||||
|
"media_player": "mdi:cast",
|
||||||
|
"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 = {
|
||||||
@ -423,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,
|
||||||
@ -3223,30 +2880,30 @@ class MaterialDesignIcons {
|
|||||||
"mdi:blank": 0xf68c
|
"mdi:blank": 0xf68c
|
||||||
};
|
};
|
||||||
|
|
||||||
static Widget createIconFromEntityData(Map data, double size, Color color) {
|
static Widget createIconWidgetFromEntityData(Entity data, double size, Color color) {
|
||||||
if ((data["attributes"] != null) && (data["attributes"]["entity_picture"] != null)) {
|
if (data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (data.entityPicture != null) {
|
||||||
if (homeAssistantWebHost != null) {
|
if (homeAssistantWebHost != null) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
|
radius: size/2,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
backgroundImage: CachedNetworkImageProvider(
|
||||||
"$homeAssistantWebHost${data["attributes"]["entity_picture"]}",
|
"$homeAssistantWebHost${data.entityPicture}",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(width: 0.0, height: 0.0);
|
return Container(width: 0.0, height: 0.0);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String iconName = data["attributes"] != null
|
String iconName = data.icon;
|
||||||
? data["attributes"]["icon"]
|
|
||||||
: null;
|
|
||||||
int iconCode = 0;
|
int iconCode = 0;
|
||||||
if (iconName != null) {
|
if (iconName.length > 0) {
|
||||||
iconCode = getIconCodeByIconName(iconName);
|
iconCode = getIconCodeByIconName(iconName);
|
||||||
} else {
|
} else {
|
||||||
iconCode = getDefaultIconByEntityId(data["entity_id"],
|
iconCode = getDefaultIconByEntityId(data.entityId,
|
||||||
data["attributes"] != null
|
data.deviceClass, data.state); //
|
||||||
? data["attributes"]["device_class"]
|
|
||||||
: null, data["state"]); //
|
|
||||||
}
|
}
|
||||||
return Icon(
|
return Icon(
|
||||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||||
@ -3270,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"];
|
191
lib/settings.page.dart
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class ConnectionSettingsPage extends StatefulWidget {
|
||||||
|
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||||
|
String _hassioDomain = "";
|
||||||
|
String _newHassioDomain = "";
|
||||||
|
String _hassioPort = "";
|
||||||
|
String _newHassioPort = "";
|
||||||
|
String _hassioPassword = "";
|
||||||
|
String _newHassioPassword = "";
|
||||||
|
String _socketProtocol = "wss";
|
||||||
|
String _newSocketProtocol = "wss";
|
||||||
|
String _authType = "access_token";
|
||||||
|
String _newAuthType = "access_token";
|
||||||
|
bool _edited = false;
|
||||||
|
FocusNode _domainFocusNode;
|
||||||
|
FocusNode _portFocusNode;
|
||||||
|
FocusNode _passwordFocusNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_domainFocusNode = FocusNode();
|
||||||
|
_portFocusNode = FocusNode();
|
||||||
|
_passwordFocusNode = FocusNode();
|
||||||
|
_domainFocusNode.addListener(_checkConfigChanged);
|
||||||
|
_portFocusNode.addListener(_checkConfigChanged);
|
||||||
|
_passwordFocusNode.addListener(_checkConfigChanged);
|
||||||
|
_loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadSettings() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
|
||||||
|
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
|
||||||
|
_hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
|
||||||
|
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
|
||||||
|
_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 {
|
||||||
|
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
||||||
|
_newHassioDomain = _newHassioDomain.split("//")[1];
|
||||||
|
}
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.setString("hassio-domain", _newHassioDomain);
|
||||||
|
prefs.setString("hassio-port", _newHassioPort);
|
||||||
|
prefs.setString("hassio-password", _newHassioPassword);
|
||||||
|
prefs.setString("hassio-protocol", _newSocketProtocol);
|
||||||
|
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
||||||
|
prefs.setString("hassio-auth-type", _newAuthType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new Scaffold(
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
|
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(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
children: <Widget>[
|
||||||
|
new Row(
|
||||||
|
children: [
|
||||||
|
Text("Use ssl (HTTPS)"),
|
||||||
|
Switch(
|
||||||
|
value: (_newSocketProtocol == "wss"),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newSocketProtocol = value ? "wss" : "ws";
|
||||||
|
_checkConfigChanged();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
new TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Home Assistant domain or ip address"
|
||||||
|
),
|
||||||
|
controller: new TextEditingController.fromValue(
|
||||||
|
new TextEditingValue(
|
||||||
|
text: _newHassioDomain,
|
||||||
|
selection:
|
||||||
|
new TextSelection.collapsed(offset: _newHassioDomain.length)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newHassioDomain = value;
|
||||||
|
},
|
||||||
|
focusNode: _domainFocusNode,
|
||||||
|
onEditingComplete: _checkConfigChanged,
|
||||||
|
),
|
||||||
|
new TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Home Assistant port (default is 8123)"
|
||||||
|
),
|
||||||
|
controller: new TextEditingController.fromValue(
|
||||||
|
new TextEditingValue(
|
||||||
|
text: _newHassioPort,
|
||||||
|
selection:
|
||||||
|
new TextSelection.collapsed(offset: _newHassioPort.length)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newHassioPort = value;
|
||||||
|
//_saveSettings();
|
||||||
|
},
|
||||||
|
focusNode: _portFocusNode,
|
||||||
|
onEditingComplete: _checkConfigChanged,
|
||||||
|
),
|
||||||
|
new Row(
|
||||||
|
children: [
|
||||||
|
Text("Login with access token (HA >= 0.78.0)"),
|
||||||
|
Switch(
|
||||||
|
value: (_newAuthType == "access_token"),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newAuthType = value ? "access_token" : "api_password";
|
||||||
|
_checkConfigChanged();
|
||||||
|
//_saveSettings();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
new TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: _authType == "access_token" ? "Access token" : "API password"
|
||||||
|
),
|
||||||
|
controller: new TextEditingController.fromValue(
|
||||||
|
new TextEditingValue(
|
||||||
|
text: _newHassioPassword,
|
||||||
|
selection:
|
||||||
|
new TextSelection.collapsed(offset: _newHassioPassword.length)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newHassioPassword = value;
|
||||||
|
//_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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,135 +0,0 @@
|
|||||||
part of 'main.dart';
|
|
||||||
|
|
||||||
class ConnectionSettingsPage extends StatefulWidget {
|
|
||||||
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|
||||||
String _hassioDomain = "";
|
|
||||||
String _hassioPort = "8123";
|
|
||||||
String _hassioPassword = "";
|
|
||||||
String _socketProtocol = "wss";
|
|
||||||
String _authType = "access_token";
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadSettings() async {
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_hassioDomain = prefs.getString("hassio-domain");
|
|
||||||
_hassioPort = prefs.getString("hassio-port") ?? '8123';
|
|
||||||
_hassioPassword = prefs.getString("hassio-password");
|
|
||||||
_socketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
|
|
||||||
_authType = prefs.getString("hassio-auth-type") ?? 'access_token';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_saveSettings() async {
|
|
||||||
if (_hassioDomain.indexOf("http") == 0 && _hassioDomain.indexOf("//") > 0) {
|
|
||||||
_hassioDomain = _hassioDomain.split("//")[1];
|
|
||||||
}
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
prefs.setString("hassio-domain", _hassioDomain);
|
|
||||||
prefs.setString("hassio-port", _hassioPort);
|
|
||||||
prefs.setString("hassio-password", _hassioPassword);
|
|
||||||
prefs.setString("hassio-protocol", _socketProtocol);
|
|
||||||
prefs.setString("hassio-res-protocol", _socketProtocol == "wss" ? "https" : "http");
|
|
||||||
prefs.setString("hassio-auth-type", _authType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return new Scaffold(
|
|
||||||
appBar: new AppBar(
|
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
|
||||||
_saveSettings().then((r){
|
|
||||||
Navigator.pop(context);
|
|
||||||
});
|
|
||||||
eventBus.fire(SettingsChangedEvent(true));
|
|
||||||
}),
|
|
||||||
// Here we take the value from the MyHomePage object that was created by
|
|
||||||
// the App.build method, and use it to set our appbar title.
|
|
||||||
title: new Text(widget.title),
|
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
padding: const EdgeInsets.all(20.0),
|
|
||||||
children: <Widget>[
|
|
||||||
new Row(
|
|
||||||
children: [
|
|
||||||
Text("HTTPS"),
|
|
||||||
Switch(
|
|
||||||
value: (_socketProtocol == "wss"),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_socketProtocol = value ? "wss" : "ws";
|
|
||||||
});
|
|
||||||
_saveSettings();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
new TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Home Assistant domain or ip address"
|
|
||||||
),
|
|
||||||
controller: TextEditingController(
|
|
||||||
text: _hassioDomain
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
_hassioDomain = value;
|
|
||||||
_saveSettings();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
new TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Home Assistant port"
|
|
||||||
),
|
|
||||||
controller: TextEditingController(
|
|
||||||
text: _hassioPort
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
_hassioPort = value;
|
|
||||||
_saveSettings();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
new Row(
|
|
||||||
children: [
|
|
||||||
Text("Login with access token (HA >= 0.78.0)"),
|
|
||||||
Switch(
|
|
||||||
value: (_authType == "access_token"),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_authType = value ? "access_token" : "api_password";
|
|
||||||
});
|
|
||||||
_saveSettings();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
new TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: _authType == "access_token" ? "Access token" : "API password"
|
|
||||||
),
|
|
||||||
controller: TextEditingController(
|
|
||||||
text: _hassioPassword
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
_hassioPassword = value;
|
|
||||||
_saveSettings();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
87
lib/utils.class.dart
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class TheLogger {
|
||||||
|
|
||||||
|
static List<String> _log = [];
|
||||||
|
|
||||||
|
static String getLog() {
|
||||||
|
String res = '';
|
||||||
|
_log.forEach((line) {
|
||||||
|
res += "$line\n";
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool get isInDebugMode {
|
||||||
|
bool inDebugMode = false;
|
||||||
|
|
||||||
|
assert(inDebugMode = true);
|
||||||
|
|
||||||
|
return inDebugMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log(String level, String message) {
|
||||||
|
if (isInDebugMode) {
|
||||||
|
debugPrint('$message');
|
||||||
|
}
|
||||||
|
_log.add("[$level] : $message");
|
||||||
|
if (_log.length > 50) {
|
||||||
|
_log.removeAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class HAUtils {
|
||||||
|
static void launchURL(String url) async {
|
||||||
|
if (await canLaunch(url)) {
|
||||||
|
await launch(url);
|
||||||
|
} else {
|
||||||
|
TheLogger.log("Error", "Could not launch $url");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateChangedEvent {
|
||||||
|
String entityId;
|
||||||
|
String newState;
|
||||||
|
bool localChange;
|
||||||
|
|
||||||
|
StateChangedEvent(this.entityId, this.newState, this.localChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsChangedEvent {
|
||||||
|
bool reconnect;
|
||||||
|
|
||||||
|
SettingsChangedEvent(this.reconnect);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RefreshDataEvent {
|
||||||
|
RefreshDataEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class RefreshDataFinishedEvent {
|
||||||
|
RefreshDataFinishedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServiceCallEvent {
|
||||||
|
String domain;
|
||||||
|
String service;
|
||||||
|
String entityId;
|
||||||
|
Map<String, dynamic> additionalParams;
|
||||||
|
|
||||||
|
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShowEntityPageEvent {
|
||||||
|
Entity entity;
|
||||||
|
|
||||||
|
ShowEntityPageEvent(this.entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShowErrorEvent {
|
||||||
|
String text;
|
||||||
|
int errorCode;
|
||||||
|
|
||||||
|
ShowErrorEvent(this.text, this.errorCode);
|
||||||
|
}
|
88
lib/view_builder.class.dart
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
168
lib/view_class.dart
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class View {
|
||||||
|
List<Entity> childEntitiesAsBadges;
|
||||||
|
Map<String, CardSkeleton> childEntitiesAsCards;
|
||||||
|
|
||||||
|
int count;
|
||||||
|
List<Entity> entities;
|
||||||
|
|
||||||
|
View({
|
||||||
|
Key key,
|
||||||
|
this.count,
|
||||||
|
this.entities
|
||||||
|
}) {
|
||||||
|
childEntitiesAsBadges = [];
|
||||||
|
childEntitiesAsCards = {};
|
||||||
|
_filterEntities();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildWidget(BuildContext context) {
|
||||||
|
return ViewWidget(
|
||||||
|
badges: childEntitiesAsBadges,
|
||||||
|
cards: childEntitiesAsCards,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _filterEntities() {
|
||||||
|
entities.forEach((Entity entity){
|
||||||
|
if (!entity.isGroup) {
|
||||||
|
if (entity.isBadge) {
|
||||||
|
childEntitiesAsBadges.add(entity);
|
||||||
|
} else {
|
||||||
|
String groupIdToAdd = "${entity.domain}.${entity.domain}$count";
|
||||||
|
if (childEntitiesAsCards[groupIdToAdd] == null) {
|
||||||
|
childEntitiesAsCards[groupIdToAdd] = CardSkeleton(
|
||||||
|
displayName: entity.domain,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
childEntitiesAsCards[groupIdToAdd].childEntities.add(entity);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
childEntitiesAsCards[entity.entityId] = CardSkeleton(
|
||||||
|
displayName: entity.displayName,
|
||||||
|
groupEntity: entity
|
||||||
|
);
|
||||||
|
childEntitiesAsCards[entity.entityId].childEntities = 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 = [];
|
||||||
|
}
|
||||||
|
}
|
38
pubspec.lock
@ -87,6 +87,13 @@ packages:
|
|||||||
url: "https://github.com/MarkOSullivan94/dart_config.git"
|
url: "https://github.com/MarkOSullivan94/dart_config.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.5.0"
|
version: "0.5.0"
|
||||||
|
date_format:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: date_format
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
event_bus:
|
event_bus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -106,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"
|
||||||
@ -244,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:
|
||||||
@ -278,7 +285,7 @@ packages:
|
|||||||
name: petitparser
|
name: petitparser
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.2"
|
||||||
plugin:
|
plugin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -320,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:
|
||||||
@ -424,6 +431,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.6"
|
version: "1.1.6"
|
||||||
|
url_launcher:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
utf:
|
utf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -472,7 +486,7 @@ packages:
|
|||||||
name: xml
|
name: xml
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.2.3"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -481,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"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
name: hass_client
|
name: hass_client
|
||||||
description: Home Assistant Android Client
|
description: Home Assistant Android Client
|
||||||
|
|
||||||
version: 0.1.1-alpha
|
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,9 +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
|
||||||
|
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.
|
||||||
@ -23,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
|
||||||
|
@ -12,7 +12,7 @@ import 'package:hass_client/main.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||||
// Build our app and trigger a frame.
|
// Build our app and trigger a frame.
|
||||||
await tester.pumpWidget(new HassClientApp());
|
await tester.pumpWidget(new HAClientApp());
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
// Verify that our counter starts at 0.
|
||||||
expect(find.text('0'), findsOneWidget);
|
expect(find.text('0'), findsOneWidget);
|
||||||
|