Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
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 |
@ -1,9 +0,0 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class Badge {
|
||||
String _entityId;
|
||||
|
||||
Badge(String groupId) {
|
||||
_entityId = groupId;
|
||||
}
|
||||
}
|
@ -1,25 +1,54 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class HACard {
|
||||
String _entityId;
|
||||
List _entities;
|
||||
String _friendlyName;
|
||||
class HACard extends StatelessWidget {
|
||||
|
||||
List get entities => _entities;
|
||||
String get friendlyName => _friendlyName;
|
||||
final List<Entity> entities;
|
||||
final String friendlyName;
|
||||
|
||||
HACard(String groupId, String friendlyName) {
|
||||
_entityId = groupId;
|
||||
_entities = [];
|
||||
_friendlyName = friendlyName;
|
||||
const HACard({
|
||||
Key key,
|
||||
this.entities,
|
||||
this.friendlyName
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> body = [];
|
||||
body.add(_buildCardHeader());
|
||||
body.addAll(_buildCardBody(context));
|
||||
return Card(
|
||||
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
|
||||
);
|
||||
}
|
||||
|
||||
void addEntity(String entityId) {
|
||||
_entities.add(entityId);
|
||||
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;
|
||||
}
|
||||
|
||||
void addEntities(List entities) {
|
||||
_entities.addAll(entities);
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class EntityViewPage extends StatefulWidget {
|
||||
EntityViewPage({Key key, this.entity}) : super(key: key);
|
||||
EntityViewPage({Key key, @required this.entity, @required this.homeAssistant }) : super(key: key);
|
||||
|
||||
final Entity entity;
|
||||
final HomeAssistant homeAssistant;
|
||||
|
||||
@override
|
||||
_EntityViewPageState createState() => new _EntityViewPageState();
|
||||
@ -11,26 +12,34 @@ class EntityViewPage extends StatefulWidget {
|
||||
|
||||
class _EntityViewPageState extends State<EntityViewPage> {
|
||||
String _title;
|
||||
Entity _entity;
|
||||
StreamSubscription _stateSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_entity = widget.entity;
|
||||
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
if (event.entityId == _entity.entityId) {
|
||||
if (event.entityId == widget.entity.entityId) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
_prepareData();
|
||||
_getHistory();
|
||||
}
|
||||
|
||||
_prepareData() async {
|
||||
_title = _entity.displayName;
|
||||
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(
|
||||
@ -44,7 +53,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
||||
),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: _entity.buildWidget(context, false)
|
||||
child: widget.entity.buildEntityPageWidget(context)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class _ButtonEntityWidgetState extends _EntityWidgetState {
|
||||
|
||||
@override
|
||||
void setNewState(newValue) {
|
||||
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "turn_on", widget.entity.entityId, null));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||
return FlatButton(
|
||||
onPressed: (() {
|
||||
setNewState(null);
|
||||
}),
|
||||
child: Text(
|
||||
"EXECUTE",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: Entity.STATE_FONT_SIZE, color: Colors.blue),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class _DateTimeEntityWidgetState extends _EntityWidgetState {
|
||||
bool get hasDate => widget.entity._attributes["has_date"] ?? false;
|
||||
bool get hasTime => widget.entity._attributes["has_time"] ?? false;
|
||||
int get year => widget.entity._attributes["year"] ?? 1970;
|
||||
int get month => widget.entity._attributes["month"] ?? 1;
|
||||
int get day => widget.entity._attributes["day"] ?? 1;
|
||||
int get hour => widget.entity._attributes["hour"] ?? 0;
|
||||
int get minute => widget.entity._attributes["minute"] ?? 0;
|
||||
int get second => widget.entity._attributes["second"] ?? 0;
|
||||
String get formattedState => _getFormattedState();
|
||||
DateTime get dateTimeState => _getDateTimeState();
|
||||
|
||||
DateTime _getDateTimeState() {
|
||||
return DateTime(this.year, this.month, this.day, this.hour, this.minute, this.second);
|
||||
}
|
||||
|
||||
String _getFormattedState() {
|
||||
String formattedState = "";
|
||||
if (this.hasDate) {
|
||||
formattedState += formatDate(dateTimeState, [M, ' ', d, ', ', yyyy]);
|
||||
}
|
||||
if (this.hasTime) {
|
||||
formattedState += " "+formatDate(dateTimeState, [HH, ':', nn]);
|
||||
}
|
||||
return formattedState;
|
||||
}
|
||||
|
||||
@override
|
||||
void setNewState(newValue) {
|
||||
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_datetime", widget.entity.entityId,
|
||||
newValue));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||
return Padding(
|
||||
padding:
|
||||
EdgeInsets.fromLTRB(0.0, 0.0, Entity.RIGHT_WIDGET_PADDING, 0.0),
|
||||
child: GestureDetector(
|
||||
child: Text(
|
||||
"$formattedState",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: Entity.STATE_FONT_SIZE,
|
||||
)),
|
||||
onTap: () => _handleStateTap(context),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _handleStateTap(BuildContext context) {
|
||||
if (hasDate) {
|
||||
_showDatePicker(context).then((date) {
|
||||
if (date != null) {
|
||||
if (hasTime) {
|
||||
_showTimePicker(context).then((time){
|
||||
setNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}", "time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
|
||||
});
|
||||
} else {
|
||||
setNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}"});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (hasTime) {
|
||||
_showTimePicker(context).then((time){
|
||||
if (time != null) {
|
||||
setNewState({"time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
TheLogger.log("Warning", "${widget.entity.entityId} has no date and no time");
|
||||
}
|
||||
}
|
||||
|
||||
Future _showDatePicker(BuildContext context) {
|
||||
return showDatePicker(
|
||||
context: context,
|
||||
initialDate: dateTimeState,
|
||||
firstDate: DateTime(1970),
|
||||
lastDate: DateTime(2037) //Unix timestamp will finish at Jan 19, 2038
|
||||
);
|
||||
}
|
||||
|
||||
Future _showTimePicker(BuildContext context) {
|
||||
return showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(dateTimeState)
|
||||
);
|
||||
}
|
||||
}
|
@ -4,44 +4,62 @@ 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 RIGHT_WIDGET_PADDING = 14.0;
|
||||
static const LEFT_WIDGET_PADDING = 8.0;
|
||||
static const EXTENDED_WIDGET_HEIGHT = 50.0;
|
||||
static const WIDGET_HEIGHT = 34.0;
|
||||
static const ICON_SIZE = 28.0;
|
||||
static const STATE_FONT_SIZE = 16.0;
|
||||
static const NAME_FONT_SIZE = 16.0;
|
||||
static const SMALL_FONT_SIZE = 14.0;
|
||||
static const INPUT_WIDTH = 160.0;
|
||||
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"
|
||||
];
|
||||
|
||||
Map _attributes;
|
||||
String _domain;
|
||||
String _entityId;
|
||||
String _state;
|
||||
double rightWidgetPadding = 14.0;
|
||||
double leftWidgetPadding = 8.0;
|
||||
double extendedWidgetHeight = 50.0;
|
||||
double widgetHeight = 34.0;
|
||||
double iconSize = 28.0;
|
||||
double stateFontSize = 16.0;
|
||||
double nameFontSize = 16.0;
|
||||
double smallFontSize = 14.0;
|
||||
double largeFontSize = 24.0;
|
||||
double inputWidth = 160.0;
|
||||
double rowPadding = 10.0;
|
||||
|
||||
Map attributes;
|
||||
String domain;
|
||||
String entityId;
|
||||
String state;
|
||||
String assumedState;
|
||||
DateTime _lastUpdated;
|
||||
|
||||
String get displayName =>
|
||||
_attributes["friendly_name"] ?? (_attributes["name"] ?? "_");
|
||||
String get domain => _domain;
|
||||
String get entityId => _entityId;
|
||||
String get state => _state;
|
||||
set state(value) => _state = value;
|
||||
List<Entity> childEntities = [];
|
||||
|
||||
String get deviceClass => _attributes["device_class"] ?? null;
|
||||
List<String> attributesToShow = ["all"];
|
||||
|
||||
String get displayName =>
|
||||
attributes["friendly_name"] ?? (attributes["name"] ?? "_");
|
||||
|
||||
String get deviceClass => attributes["device_class"] ?? null;
|
||||
bool get isView =>
|
||||
(_domain == "group") &&
|
||||
(_attributes != null ? _attributes["view"] ?? false : false);
|
||||
bool get isGroup => _domain == "group";
|
||||
String get icon => _attributes["icon"] ?? "";
|
||||
(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 childEntities => _attributes["entity_id"] ?? [];
|
||||
String get entityPicture => attributes["entity_picture"];
|
||||
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
|
||||
List get childEntityIds => attributes["entity_id"] ?? [];
|
||||
String get lastUpdated => _getLastUpdatedFormatted();
|
||||
|
||||
Entity(Map rawData) {
|
||||
@ -49,21 +67,78 @@ class Entity {
|
||||
}
|
||||
|
||||
void update(Map rawData) {
|
||||
_attributes = rawData["attributes"] ?? {};
|
||||
_domain = rawData["entity_id"].split(".")[0];
|
||||
_entityId = rawData["entity_id"];
|
||||
_state = rawData["state"];
|
||||
assumedState = _state;
|
||||
attributes = rawData["attributes"] ?? {};
|
||||
domain = rawData["entity_id"].split(".")[0];
|
||||
entityId = rawData["entity_id"];
|
||||
state = rawData["state"];
|
||||
assumedState = state;
|
||||
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
||||
}
|
||||
|
||||
EntityWidget buildWidget(BuildContext context, bool inCard) {
|
||||
return EntityWidget(
|
||||
double _getDoubleAttributeValue(String attributeName) {
|
||||
var temp1 = attributes["$attributeName"];
|
||||
if (temp1 is int) {
|
||||
return temp1.toDouble();
|
||||
} else if (temp1 is double) {
|
||||
return temp1;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildDefaultWidget(BuildContext context) {
|
||||
return EntityModel(
|
||||
entity: this,
|
||||
inCard: inCard,
|
||||
child: DefaultEntityContainer(state: _buildStatePart(context)),
|
||||
handleTap: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return SimpleEntityState();
|
||||
}
|
||||
|
||||
Widget _buildStatePartForPage(BuildContext context) {
|
||||
return _buildStatePart(context);
|
||||
}
|
||||
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
return Container(
|
||||
width: 0.0,
|
||||
height: 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildEntityPageWidget(BuildContext context) {
|
||||
return EntityModel(
|
||||
entity: this,
|
||||
child: EntityPageContainer(children: <Widget>[
|
||||
DefaultEntityContainer(state: _buildStatePartForPage(context)),
|
||||
LastUpdatedWidget(),
|
||||
Divider(),
|
||||
_buildAdditionalControlsForPage(context),
|
||||
Divider(),
|
||||
EntityAttributesList()
|
||||
]),
|
||||
handleTap: false,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBadgeWidget(BuildContext context) {
|
||||
return EntityModel(
|
||||
entity: this,
|
||||
child: Badge(),
|
||||
handleTap: true,
|
||||
);
|
||||
}
|
||||
|
||||
String getAttribute(String attributeName) {
|
||||
if (attributes != null) {
|
||||
return attributes["$attributeName"];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _getLastUpdatedFormatted() {
|
||||
if (_lastUpdated == null) {
|
||||
return "-";
|
||||
@ -92,151 +167,300 @@ class Entity {
|
||||
return "$v $text";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class EntityWidget extends StatefulWidget {
|
||||
EntityWidget({Key key, this.entity, this.inCard}) : super(key: key);
|
||||
|
||||
final Entity entity;
|
||||
final bool inCard;
|
||||
class SwitchEntity extends Entity {
|
||||
SwitchEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
_EntityWidgetState createState() {
|
||||
switch (entity.domain) {
|
||||
case "automation":
|
||||
case "input_boolean":
|
||||
case "switch":
|
||||
case "light": {
|
||||
return _SwitchEntityWidgetState();
|
||||
}
|
||||
|
||||
case "script":
|
||||
case "scene": {
|
||||
return _ButtonEntityWidgetState();
|
||||
}
|
||||
|
||||
case "input_datetime": {
|
||||
return _DateTimeEntityWidgetState();
|
||||
}
|
||||
|
||||
case "input_select": {
|
||||
return _SelectEntityWidgetState();
|
||||
}
|
||||
|
||||
case "input_number": {
|
||||
return _SliderEntityWidgetState();
|
||||
}
|
||||
|
||||
case "input_text": {
|
||||
return _TextEntityWidgetState();
|
||||
}
|
||||
|
||||
default: {
|
||||
return _EntityWidgetState();
|
||||
}
|
||||
}
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return SwitchControlWidget();
|
||||
}
|
||||
}
|
||||
|
||||
class _EntityWidgetState extends State<EntityWidget> {
|
||||
class ButtonEntity extends Entity {
|
||||
ButtonEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.inCard) {
|
||||
return _buildMainWidget(context);
|
||||
} else {
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
_buildMainWidget(context),
|
||||
_buildLastUpdatedWidget()
|
||||
],
|
||||
);
|
||||
}
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return ButtonControlWidget();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildMainWidget(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: Entity.WIDGET_HEIGHT,
|
||||
class TextEntity extends Entity {
|
||||
TextEntity(Map rawData) : super(rawData);
|
||||
|
||||
int get valueMinLength => attributes["min"] ?? -1;
|
||||
int get valueMaxLength => attributes["max"] ?? -1;
|
||||
String get valuePattern => attributes["pattern"] ?? null;
|
||||
bool get isTextField => attributes["mode"] == "text";
|
||||
bool get isPasswordField => attributes["mode"] == "password";
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return TextControlWidget();
|
||||
}
|
||||
}
|
||||
|
||||
class SunEntity extends Entity {
|
||||
SunEntity(Map rawData) : super(rawData);
|
||||
}
|
||||
|
||||
class SliderEntity extends Entity {
|
||||
SliderEntity(Map rawData) : super(rawData);
|
||||
|
||||
double get minValue => attributes["min"] ?? 0.0;
|
||||
double get maxValue => attributes["max"] ?? 100.0;
|
||||
double get valueStep => attributes["step"] ?? 1.0;
|
||||
double get doubleState => double.tryParse(state) ?? 0.0;
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return Expanded(
|
||||
//width: 200.0,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
child: _buildIconWidget(),
|
||||
onTap: widget.inCard ? openEntityPage : null,
|
||||
SliderControlWidget(
|
||||
expanded: true,
|
||||
),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
child: _buildNameWidget(),
|
||||
onTap: widget.inCard ? openEntityPage : null,
|
||||
),
|
||||
),
|
||||
_buildActionWidget(widget.inCard, context)
|
||||
SimpleEntityState(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void openEntityPage() {
|
||||
eventBus.fire(new ShowEntityPageEvent(widget.entity));
|
||||
@override
|
||||
Widget _buildStatePartForPage(BuildContext context) {
|
||||
return SimpleEntityState();
|
||||
}
|
||||
|
||||
void setNewState(newState) {
|
||||
return;
|
||||
}
|
||||
|
||||
Widget buildAdditionalWidget() {
|
||||
return _buildLastUpdatedWidget();
|
||||
}
|
||||
|
||||
Widget _buildIconWidget() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(Entity.LEFT_WIDGET_PADDING, 0.0, 12.0, 0.0),
|
||||
child: MaterialDesignIcons.createIconWidgetFromEntityData(
|
||||
widget.entity,
|
||||
Entity.ICON_SIZE,
|
||||
Entity.STATE_ICONS_COLORS[widget.entity.state] ?? Colors.blueGrey),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLastUpdatedWidget() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
Entity.LEFT_WIDGET_PADDING, Entity.SMALL_FONT_SIZE, 0.0, 0.0),
|
||||
child: Text(
|
||||
'${widget.entity.lastUpdated}',
|
||||
textAlign: TextAlign.left,
|
||||
style:
|
||||
TextStyle(fontSize: Entity.SMALL_FONT_SIZE, color: Colors.black26),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNameWidget() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 10.0),
|
||||
child: Text(
|
||||
"${widget.entity.displayName}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
style: TextStyle(fontSize: Entity.NAME_FONT_SIZE),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||
return Padding(
|
||||
padding:
|
||||
EdgeInsets.fromLTRB(0.0, 0.0, Entity.RIGHT_WIDGET_PADDING, 0.0),
|
||||
child: GestureDetector(
|
||||
child: Text(
|
||||
"${widget.entity.state}${widget.entity.unitOfMeasurement}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: Entity.STATE_FONT_SIZE,
|
||||
)),
|
||||
onTap: openEntityPage,
|
||||
)
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
return SliderControlWidget(
|
||||
expanded: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ClimateEntity extends Entity {
|
||||
@override
|
||||
double widgetHeight = 38.0;
|
||||
|
||||
static const SUPPORT_TARGET_TEMPERATURE = 1;
|
||||
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
|
||||
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
|
||||
static const SUPPORT_TARGET_HUMIDITY = 8;
|
||||
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
|
||||
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
|
||||
static const SUPPORT_FAN_MODE = 64;
|
||||
static const SUPPORT_OPERATION_MODE = 128;
|
||||
static const SUPPORT_HOLD_MODE = 256;
|
||||
static const SUPPORT_SWING_MODE = 512;
|
||||
static const SUPPORT_AWAY_MODE = 1024;
|
||||
static const SUPPORT_AUX_HEAT = 2048;
|
||||
static const SUPPORT_ON_OFF = 4096;
|
||||
|
||||
bool get supportTargetTemperature => ((attributes["supported_features"] &
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
|
||||
bool get supportTargetTemperatureHigh => ((attributes["supported_features"] &
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
|
||||
bool get supportTargetTemperatureLow => ((attributes["supported_features"] &
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
|
||||
bool get supportTargetHumidity => ((attributes["supported_features"] &
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
|
||||
bool get supportTargetHumidityHigh => ((attributes["supported_features"] &
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
|
||||
bool get supportTargetHumidityLow => ((attributes["supported_features"] &
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
|
||||
bool get supportFanMode =>
|
||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_FAN_MODE) ==
|
||||
ClimateEntity.SUPPORT_FAN_MODE);
|
||||
bool get supportOperationMode => ((attributes["supported_features"] &
|
||||
ClimateEntity.SUPPORT_OPERATION_MODE) ==
|
||||
ClimateEntity.SUPPORT_OPERATION_MODE);
|
||||
bool get supportHoldMode =>
|
||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_HOLD_MODE) ==
|
||||
ClimateEntity.SUPPORT_HOLD_MODE);
|
||||
bool get supportSwingMode =>
|
||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_SWING_MODE) ==
|
||||
ClimateEntity.SUPPORT_SWING_MODE);
|
||||
bool get supportAwayMode =>
|
||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_AWAY_MODE) ==
|
||||
ClimateEntity.SUPPORT_AWAY_MODE);
|
||||
bool get supportAuxHeat =>
|
||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_AUX_HEAT) ==
|
||||
ClimateEntity.SUPPORT_AUX_HEAT);
|
||||
bool get supportOnOff =>
|
||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_ON_OFF) ==
|
||||
ClimateEntity.SUPPORT_ON_OFF);
|
||||
|
||||
List<String> get operationList => attributes["operation_list"] != null
|
||||
? (attributes["operation_list"] as List).cast<String>()
|
||||
: null;
|
||||
List<String> get fanList => attributes["fan_list"] != null
|
||||
? (attributes["fan_list"] as List).cast<String>()
|
||||
: null;
|
||||
List<String> get swingList => attributes["swing_list"] != null
|
||||
? (attributes["swing_list"] as List).cast<String>()
|
||||
: null;
|
||||
double get temperature => _getDoubleAttributeValue('temperature');
|
||||
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
|
||||
double get targetLow => _getDoubleAttributeValue('target_temp_low');
|
||||
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
|
||||
double get minTemp => _getDoubleAttributeValue('min_temp') ?? -100.0;
|
||||
double get targetHumidity => _getDoubleAttributeValue('humidity');
|
||||
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
|
||||
double get minHumidity => _getDoubleAttributeValue('min_humidity');
|
||||
String get operationMode => attributes['operation_mode'];
|
||||
String get fanMode => attributes['fan_mode'];
|
||||
String get swingMode => attributes['swing_mode'];
|
||||
bool get awayMode => attributes['away_mode'] == "on";
|
||||
bool get isOff => state == "off";
|
||||
bool get auxHeat => attributes['aux_heat'] == "on";
|
||||
|
||||
ClimateEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return ClimateStateWidget();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
return ClimateControlWidget();
|
||||
}
|
||||
|
||||
@override
|
||||
double _getDoubleAttributeValue(String attributeName) {
|
||||
var temp1 = attributes["$attributeName"];
|
||||
if (temp1 is int) {
|
||||
return temp1.toDouble();
|
||||
} else if (temp1 is double) {
|
||||
return temp1;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SelectEntity extends Entity {
|
||||
List<String> get listOptions => attributes["options"] != null
|
||||
? (attributes["options"] as List).cast<String>()
|
||||
: [];
|
||||
|
||||
SelectEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return SelectControlWidget();
|
||||
}
|
||||
}
|
||||
|
||||
class DateTimeEntity extends Entity {
|
||||
bool get hasDate => attributes["has_date"] ?? false;
|
||||
bool get hasTime => attributes["has_time"] ?? false;
|
||||
int get year => attributes["year"] ?? 1970;
|
||||
int get month => attributes["month"] ?? 1;
|
||||
int get day => attributes["day"] ?? 1;
|
||||
int get hour => attributes["hour"] ?? 0;
|
||||
int get minute => attributes["minute"] ?? 0;
|
||||
int get second => attributes["second"] ?? 0;
|
||||
String get formattedState => _getFormattedState();
|
||||
DateTime get dateTimeState => _getDateTimeState();
|
||||
|
||||
DateTimeEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return DateTimeStateWidget();
|
||||
}
|
||||
|
||||
DateTime _getDateTimeState() {
|
||||
return DateTime(
|
||||
this.year, this.month, this.day, this.hour, this.minute, this.second);
|
||||
}
|
||||
|
||||
String _getFormattedState() {
|
||||
String formattedState = "";
|
||||
if (this.hasDate) {
|
||||
formattedState += formatDate(dateTimeState, [M, ' ', d, ', ', yyyy]);
|
||||
}
|
||||
if (this.hasTime) {
|
||||
formattedState += " " + formatDate(dateTimeState, [HH, ':', nn]);
|
||||
}
|
||||
return formattedState;
|
||||
}
|
||||
|
||||
void setNewState(newValue) {
|
||||
eventBus
|
||||
.fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue));
|
||||
}
|
||||
}
|
||||
|
||||
class CoverEntity extends Entity {
|
||||
@override
|
||||
double widgetHeight = 38.0;
|
||||
|
||||
static const SUPPORT_OPEN = 1;
|
||||
static const SUPPORT_CLOSE = 2;
|
||||
static const SUPPORT_SET_POSITION = 4;
|
||||
static const SUPPORT_STOP = 8;
|
||||
static const SUPPORT_OPEN_TILT = 16;
|
||||
static const SUPPORT_CLOSE_TILT = 32;
|
||||
static const SUPPORT_STOP_TILT = 64;
|
||||
static const SUPPORT_SET_TILT_POSITION = 128;
|
||||
|
||||
bool get supportOpen => ((attributes["supported_features"] &
|
||||
CoverEntity.SUPPORT_OPEN) ==
|
||||
CoverEntity.SUPPORT_OPEN);
|
||||
bool get supportClose => ((attributes["supported_features"] &
|
||||
CoverEntity.SUPPORT_CLOSE) ==
|
||||
CoverEntity.SUPPORT_CLOSE);
|
||||
bool get supportSetPosition => ((attributes["supported_features"] &
|
||||
CoverEntity.SUPPORT_SET_POSITION) ==
|
||||
CoverEntity.SUPPORT_SET_POSITION);
|
||||
bool get supportStop => ((attributes["supported_features"] &
|
||||
CoverEntity.SUPPORT_STOP) ==
|
||||
CoverEntity.SUPPORT_STOP);
|
||||
|
||||
bool get supportOpenTilt => ((attributes["supported_features"] &
|
||||
CoverEntity.SUPPORT_OPEN_TILT) ==
|
||||
CoverEntity.SUPPORT_OPEN_TILT);
|
||||
bool get supportCloseTilt => ((attributes["supported_features"] &
|
||||
CoverEntity.SUPPORT_CLOSE_TILT) ==
|
||||
CoverEntity.SUPPORT_CLOSE_TILT);
|
||||
bool get supportStopTilt => ((attributes["supported_features"] &
|
||||
CoverEntity.SUPPORT_STOP_TILT) ==
|
||||
CoverEntity.SUPPORT_STOP_TILT);
|
||||
bool get supportSetTiltPosition => ((attributes["supported_features"] &
|
||||
CoverEntity.SUPPORT_SET_TILT_POSITION) ==
|
||||
CoverEntity.SUPPORT_SET_TILT_POSITION);
|
||||
|
||||
|
||||
double get currentPosition => _getDoubleAttributeValue('current_position');
|
||||
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position');
|
||||
bool get canBeOpened => ((state == "closed") || (state == "closing") || (state == "opening"));
|
||||
bool get canBeClosed => ((state == "open") || (state == "opening")|| (state == "closing"));
|
||||
bool get canTiltBeOpened => currentPosition < 100;
|
||||
bool get canTiltBeClosed => currentPosition > 0;
|
||||
|
||||
CoverEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return CoverEntityControlState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
return CoverControlWidget();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class _SelectEntityWidgetState extends _EntityWidgetState {
|
||||
List<String> _listOptions = [];
|
||||
|
||||
@override
|
||||
void setNewState(newValue) {
|
||||
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "select_option", widget.entity.entityId,
|
||||
{"option": "$newValue"}));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||
_listOptions.clear();
|
||||
if (widget.entity._attributes["options"] != null) {
|
||||
widget.entity._attributes["options"].forEach((value){
|
||||
_listOptions.add(value.toString());
|
||||
});
|
||||
}
|
||||
return Expanded(
|
||||
//width: Entity.INPUT_WIDTH,
|
||||
child: DropdownButton<String>(
|
||||
value: widget.entity.state,
|
||||
items: this._listOptions.map((String value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: new Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (_) {
|
||||
setNewState(_);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class _SliderEntityWidgetState extends _EntityWidgetState {
|
||||
int _multiplier = 1;
|
||||
|
||||
double get minValue => widget.entity._attributes["min"] ?? 0.0;
|
||||
double get maxValue => widget.entity._attributes["max"] ?? 100.0;
|
||||
double get valueStep => widget.entity._attributes["step"] ?? 1.0;
|
||||
double get doubleState => double.tryParse(widget.entity.state) ?? 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void setNewState(newValue) {
|
||||
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_value", widget.entity.entityId,
|
||||
{"value": "${newValue.toString()}"}));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||
if (valueStep < 1) {
|
||||
_multiplier = 10;
|
||||
} else if (valueStep < 0.1) {
|
||||
_multiplier = 100;
|
||||
}
|
||||
return Expanded(
|
||||
//width: 200.0,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Slider(
|
||||
min: this.minValue * _multiplier,
|
||||
max: this.maxValue * _multiplier,
|
||||
value: (doubleState <= this.maxValue) &&
|
||||
(doubleState >= this.minValue)
|
||||
? doubleState * _multiplier
|
||||
: this.minValue * _multiplier,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
widget.entity.state = (value.roundToDouble() / _multiplier).toString();
|
||||
});
|
||||
/*eventBus.fire(new StateChangedEvent(widget.entity.entityId,
|
||||
(value.roundToDouble() / _multiplier).toString(), true));*/
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
setNewState(value.roundToDouble() / _multiplier);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: Entity.RIGHT_WIDGET_PADDING),
|
||||
child: Text("${widget.entity.state}${widget.entity.unitOfMeasurement}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: Entity.STATE_FONT_SIZE,
|
||||
)),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
954
lib/entity_class/stateful_widgets.dart
Normal file
@ -0,0 +1,954 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class SwitchControlWidget extends StatefulWidget {
|
||||
@override
|
||||
_SwitchControlWidgetState createState() => _SwitchControlWidgetState();
|
||||
}
|
||||
|
||||
class _SwitchControlWidgetState extends State<SwitchControlWidget> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _setNewState(newValue, Entity entity) {
|
||||
setState(() {
|
||||
entity.assumedState = newValue ? 'on' : 'off';
|
||||
});
|
||||
Timer(Duration(seconds: 2), (){
|
||||
setState(() {
|
||||
entity.assumedState = entity.state;
|
||||
});
|
||||
});
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
return Switch(
|
||||
value: entityModel.entity.assumedState == 'on',
|
||||
onChanged: ((switchState) {
|
||||
_setNewState(switchState, entityModel.entity);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonControlWidget extends StatefulWidget {
|
||||
@override
|
||||
_ButtonControlWidgetState createState() => _ButtonControlWidgetState();
|
||||
}
|
||||
|
||||
class _ButtonControlWidgetState extends State<ButtonControlWidget> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _setNewState(Entity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "turn_on", entity.entityId, null));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
return FlatButton(
|
||||
onPressed: (() {
|
||||
_setNewState(entityModel.entity);
|
||||
}),
|
||||
child: Text(
|
||||
"EXECUTE",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: entityModel.entity.stateFontSize, color: Colors.blue),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TextControlWidget extends StatefulWidget {
|
||||
|
||||
TextControlWidget({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_TextControlWidgetState createState() => _TextControlWidgetState();
|
||||
}
|
||||
|
||||
class _TextControlWidgetState extends State<TextControlWidget> {
|
||||
String _tmpValue;
|
||||
String _entityState;
|
||||
String _entityDomain;
|
||||
String _entityId;
|
||||
int _minLength;
|
||||
int _maxLength;
|
||||
FocusNode _focusNode = FocusNode();
|
||||
bool validValue = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode.addListener(_focusListener);
|
||||
}
|
||||
|
||||
void setNewState(newValue, domain, entityId) {
|
||||
if (validate(newValue, _minLength, _maxLength)) {
|
||||
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
|
||||
{"value": "$newValue"}));
|
||||
} else {
|
||||
setState(() {
|
||||
_tmpValue = _entityState;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool validate(newValue, minLength, maxLength) {
|
||||
if (newValue is String) {
|
||||
validValue = (newValue.length >= minLength) &&
|
||||
(maxLength == -1 ||
|
||||
(newValue.length <= maxLength));
|
||||
} else {
|
||||
validValue = true;
|
||||
}
|
||||
return validValue;
|
||||
}
|
||||
|
||||
void _focusListener() {
|
||||
if (!_focusNode.hasFocus && (_tmpValue != _entityState)) {
|
||||
setNewState(_tmpValue, _entityDomain, _entityId);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final TextEntity entity = entityModel.entity;
|
||||
_entityState = entity.state;
|
||||
_entityDomain = entity.domain;
|
||||
_entityId = entity.entityId;
|
||||
_minLength = entity.valueMinLength;
|
||||
_maxLength = entity.valueMaxLength;
|
||||
|
||||
if (!_focusNode.hasFocus && (_tmpValue != entity.state)) {
|
||||
_tmpValue = entity.state;
|
||||
}
|
||||
if (entity.isTextField || entity.isPasswordField) {
|
||||
return Expanded(
|
||||
//width: Entity.INPUT_WIDTH,
|
||||
child: TextField(
|
||||
focusNode: _focusNode,
|
||||
obscureText: entity.isPasswordField,
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _tmpValue,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _tmpValue.length)
|
||||
)
|
||||
),
|
||||
onChanged: (value) {
|
||||
_tmpValue = value;
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
TheLogger.log("Warning", "Unsupported input mode for ${entity.entityId}");
|
||||
return SimpleEntityState();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(_focusListener);
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SliderControlWidget extends StatefulWidget {
|
||||
|
||||
final bool expanded;
|
||||
|
||||
SliderControlWidget({Key key, @required this.expanded}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SliderControlWidgetState createState() => _SliderControlWidgetState();
|
||||
}
|
||||
|
||||
class _SliderControlWidgetState extends State<SliderControlWidget> {
|
||||
int _multiplier = 1;
|
||||
|
||||
void setNewState(newValue, domain, entityId) {
|
||||
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
|
||||
{"value": "${newValue.toString()}"}));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final SliderEntity entity = entityModel.entity;
|
||||
if (entity.valueStep < 1) {
|
||||
_multiplier = 10;
|
||||
} else if (entity.valueStep < 0.1) {
|
||||
_multiplier = 100;
|
||||
}
|
||||
Widget slider = Slider(
|
||||
min: entity.minValue * _multiplier,
|
||||
max: entity.maxValue * _multiplier,
|
||||
value: (entity.doubleState <= entity.maxValue) &&
|
||||
(entity.doubleState >= entity.minValue)
|
||||
? entity.doubleState * _multiplier
|
||||
: entity.minValue * _multiplier,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
entity.state =
|
||||
(value.roundToDouble() / _multiplier).toString();
|
||||
});
|
||||
eventBus.fire(new StateChangedEvent(entity.entityId,
|
||||
(value.roundToDouble() / _multiplier).toString(), true));
|
||||
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
setNewState(value.roundToDouble() / _multiplier, entity.domain, entity.entityId);
|
||||
},
|
||||
);
|
||||
if (widget.expanded) {
|
||||
return Expanded(
|
||||
child: slider,
|
||||
);
|
||||
} else {
|
||||
return slider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ClimateControlWidget extends StatefulWidget {
|
||||
|
||||
ClimateControlWidget({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ClimateControlWidgetState createState() => _ClimateControlWidgetState();
|
||||
}
|
||||
|
||||
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
|
||||
bool _showPending = false;
|
||||
bool _changedHere = false;
|
||||
Timer _resetTimer;
|
||||
double _tmpTemperature = 0.0;
|
||||
double _tmpTargetLow = 0.0;
|
||||
double _tmpTargetHigh = 0.0;
|
||||
double _tmpTargetHumidity = 0.0;
|
||||
String _tmpOperationMode;
|
||||
String _tmpFanMode;
|
||||
String _tmpSwingMode;
|
||||
bool _tmpAwayMode = false;
|
||||
bool _tmpIsOff = false;
|
||||
bool _tmpAuxHeat = false;
|
||||
|
||||
void _resetVars(ClimateEntity entity) {
|
||||
_tmpTemperature = entity.temperature;
|
||||
_tmpTargetHigh = entity.targetHigh;
|
||||
_tmpTargetLow = entity.targetLow;
|
||||
_tmpOperationMode = entity.operationMode;
|
||||
_tmpFanMode = entity.fanMode;
|
||||
_tmpSwingMode = entity.swingMode;
|
||||
_tmpAwayMode = entity.awayMode;
|
||||
_tmpIsOff = entity.isOff;
|
||||
_tmpAuxHeat = entity.auxHeat;
|
||||
_tmpTargetHumidity = entity.targetHumidity;
|
||||
|
||||
_showPending = false;
|
||||
_changedHere = false;
|
||||
}
|
||||
|
||||
void _temperatureUp(ClimateEntity entity, double step) {
|
||||
_tmpTemperature = ((_tmpTemperature + step) <= entity.maxTemp) ? _tmpTemperature + step : entity.maxTemp;
|
||||
_setTemperature(entity);
|
||||
}
|
||||
|
||||
void _temperatureDown(ClimateEntity entity, double step) {
|
||||
_tmpTemperature = ((_tmpTemperature - step) >= entity.minTemp) ? _tmpTemperature - step : entity.minTemp;
|
||||
_setTemperature(entity);
|
||||
}
|
||||
|
||||
void _targetLowUp(ClimateEntity entity, double step) {
|
||||
_tmpTargetLow = ((_tmpTargetLow + step) <= entity.maxTemp) ? _tmpTargetLow + step : entity.maxTemp;
|
||||
_setTargetTemp(entity);
|
||||
}
|
||||
|
||||
void _targetLowDown(ClimateEntity entity, double step) {
|
||||
_tmpTargetLow = ((_tmpTargetLow - step) >= entity.minTemp) ? _tmpTargetLow - step : entity.minTemp;
|
||||
_setTargetTemp(entity);
|
||||
}
|
||||
|
||||
void _targetHighUp(ClimateEntity entity, double step) {
|
||||
_tmpTargetHigh = ((_tmpTargetHigh + step) <= entity.maxTemp) ? _tmpTargetHigh + step : entity.maxTemp;
|
||||
_setTargetTemp(entity);
|
||||
}
|
||||
|
||||
void _targetHighDown(ClimateEntity entity, double step) {
|
||||
_tmpTargetHigh = ((_tmpTargetHigh - step) >= entity.minTemp) ? _tmpTargetHigh - step : entity.minTemp;
|
||||
_setTargetTemp(entity);
|
||||
}
|
||||
|
||||
void _setTemperature(ClimateEntity entity) {
|
||||
setState(() {
|
||||
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void _setTargetTemp(ClimateEntity entity) {
|
||||
setState(() {
|
||||
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
|
||||
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void _setTargetHumidity(ClimateEntity entity, double value) {
|
||||
setState(() {
|
||||
_tmpTargetHumidity = value.roundToDouble();
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_humidity", entity.entityId,{"humidity": "$_tmpTargetHumidity"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void _setOperationMode(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
_tmpOperationMode = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_operation_mode", entity.entityId,{"operation_mode": "$_tmpOperationMode"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void _setSwingMode(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
_tmpSwingMode = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_swing_mode", entity.entityId,{"swing_mode": "$_tmpSwingMode"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void _setFanMode(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
_tmpFanMode = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_fan_mode", entity.entityId,{"fan_mode": "$_tmpFanMode"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void _setAwayMode(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
_tmpAwayMode = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_away_mode", entity.entityId,{"away_mode": "${_tmpAwayMode ? 'on' : 'off'}"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void _setOnOf(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
_tmpIsOff = !value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void _setAuxHeat(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
_tmpAuxHeat = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void _resetStateTimer(ClimateEntity entity) {
|
||||
if (_resetTimer!=null) {
|
||||
_resetTimer.cancel();
|
||||
}
|
||||
_resetTimer = Timer(Duration(seconds: 3), () {
|
||||
setState(() {});
|
||||
_resetVars(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final ClimateEntity entity = entityModel.entity;
|
||||
if (_changedHere) {
|
||||
_showPending = (_tmpTemperature != entity.temperature);
|
||||
_changedHere = false;
|
||||
} else {
|
||||
_resetTimer?.cancel();
|
||||
_resetVars(entity);
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(entity.leftWidgetPadding, entity.rowPadding, entity.rightWidgetPadding, 0.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
_buildOnOffControl(entity),
|
||||
_buildTemperatureControls(entity),
|
||||
_buildHumidityControls(entity),
|
||||
_buildOperationControl(entity),
|
||||
_buildFanControl(entity),
|
||||
_buildSwingControl(entity),
|
||||
_buildAwayModeControl(entity),
|
||||
_buildAuxHeatControl(entity)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAwayModeControl(ClimateEntity entity) {
|
||||
if (entity.supportAwayMode) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Away mode",
|
||||
style: TextStyle(
|
||||
fontSize: entity.stateFontSize
|
||||
),
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
onChanged: (value) => _setAwayMode(entity, value),
|
||||
value: _tmpAwayMode,
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildOnOffControl(ClimateEntity entity) {
|
||||
if (entity.supportOnOff) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
"On / Off",
|
||||
style: TextStyle(
|
||||
fontSize: entity.stateFontSize
|
||||
),
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
onChanged: (value) => _setOnOf(entity, value),
|
||||
value: !_tmpIsOff,
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildAuxHeatControl(ClimateEntity entity) {
|
||||
if (entity.supportAuxHeat ) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Aux heat",
|
||||
style: TextStyle(
|
||||
fontSize: entity.stateFontSize
|
||||
),
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
onChanged: (value) => _setAuxHeat(entity, value),
|
||||
value: _tmpAuxHeat,
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildOperationControl(ClimateEntity entity) {
|
||||
if (entity.supportOperationMode) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("Operation", style: TextStyle(
|
||||
fontSize: entity.stateFontSize
|
||||
)),
|
||||
DropdownButton<String>(
|
||||
value: "$_tmpOperationMode",
|
||||
iconSize: 30.0,
|
||||
style: TextStyle(
|
||||
fontSize: entity.largeFontSize,
|
||||
color: Colors.black,
|
||||
),
|
||||
items: entity.operationList.map((String value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: new Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (mode) => _setOperationMode(entity, mode),
|
||||
),
|
||||
Container(height: entity.rowPadding,)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Container(height: 0.0, width: 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFanControl(ClimateEntity entity) {
|
||||
if (entity.supportFanMode) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("Fan mode", style: TextStyle(
|
||||
fontSize: entity.stateFontSize
|
||||
)),
|
||||
DropdownButton<String>(
|
||||
value: "$_tmpFanMode",
|
||||
iconSize: 30.0,
|
||||
style: TextStyle(
|
||||
fontSize: entity.largeFontSize,
|
||||
color: Colors.black,
|
||||
),
|
||||
items: entity.fanList.map((String value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: new Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (mode) => _setFanMode(entity, mode),
|
||||
),
|
||||
Container(height: entity.rowPadding,)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Container(height: 0.0, width: 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSwingControl(ClimateEntity entity) {
|
||||
if (entity.supportSwingMode) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("Swing mode", style: TextStyle(
|
||||
fontSize: entity.stateFontSize
|
||||
)),
|
||||
DropdownButton<String>(
|
||||
value: "$_tmpSwingMode",
|
||||
iconSize: 30.0,
|
||||
style: TextStyle(
|
||||
fontSize: entity.largeFontSize,
|
||||
color: Colors.black,
|
||||
),
|
||||
items: entity.swingList.map((String value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: new Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (mode) => _setSwingMode(entity, mode),
|
||||
),
|
||||
Container(height: entity.rowPadding,)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Container(height: 0.0, width: 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTemperatureControls(ClimateEntity entity) {
|
||||
List<Widget> result = [];
|
||||
if (entity.supportTargetTemperature) {
|
||||
result.addAll(<Widget>[
|
||||
Text(
|
||||
"$_tmpTemperature",
|
||||
style: TextStyle(
|
||||
fontSize: entity.largeFontSize,
|
||||
color: _showPending ? Colors.red : Colors.black
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-up')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => _temperatureUp(entity, 0.1),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-down')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => _temperatureDown(entity, 0.1),
|
||||
)
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-up')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => _temperatureUp(entity, 0.5),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-down')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => _temperatureDown(entity, 0.5),
|
||||
)
|
||||
],
|
||||
)
|
||||
]);
|
||||
} else if (entity.supportTargetTemperatureHigh && entity.supportTargetTemperatureLow) {
|
||||
result.addAll(<Widget>[
|
||||
Text(
|
||||
"$_tmpTargetLow",
|
||||
style: TextStyle(
|
||||
fontSize: entity.largeFontSize,
|
||||
color: _showPending ? Colors.red : Colors.black
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-up')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => _targetLowUp(entity, 0.1),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-down')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => _targetLowDown(entity, 0.1),
|
||||
)
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-up')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => _targetLowUp(entity, 0.5),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-down')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => _targetLowDown(entity, 0.5),
|
||||
)
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Container(height: 10.0),
|
||||
),
|
||||
Text(
|
||||
"$_tmpTargetHigh",
|
||||
style: TextStyle(
|
||||
fontSize: entity.largeFontSize,
|
||||
color: _showPending ? Colors.red : Colors.black
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-up')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => _targetHighUp(entity, 0.1),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-down')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => _targetHighDown(entity, 0.1),
|
||||
)
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-up')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => _targetHighUp(entity, 0.5),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-down')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => _targetHighDown(entity, 0.5),
|
||||
)
|
||||
],
|
||||
)
|
||||
]);
|
||||
} else if (entity.supportTargetTemperatureHigh || entity.supportTargetTemperatureLow) {
|
||||
result.add(Text("Unsupported temperature control. Please, report an issue."));
|
||||
}
|
||||
if (result.isNotEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("Target temperature", style: TextStyle(
|
||||
fontSize: entity.stateFontSize
|
||||
)),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: result,
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHumidityControls(ClimateEntity entity) {
|
||||
List<Widget> result = [];
|
||||
if (entity.supportTargetHumidity) {
|
||||
result.addAll(<Widget>[
|
||||
Text(
|
||||
"$_tmpTargetHumidity%",
|
||||
style: TextStyle(fontSize: entity.largeFontSize),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: _tmpTargetHumidity,
|
||||
max: entity.maxHumidity,
|
||||
min: entity.minHumidity,
|
||||
onChanged: ((double val) {
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_tmpTargetHumidity = val.roundToDouble();
|
||||
});
|
||||
}),
|
||||
onChangeEnd: (double v) => _setTargetHumidity(entity, v),
|
||||
),
|
||||
)
|
||||
]);
|
||||
}
|
||||
if (result.isNotEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
0.0, entity.rowPadding, 0.0, entity.rowPadding),
|
||||
child: Text("Target humidity", style: TextStyle(
|
||||
fontSize: entity.stateFontSize
|
||||
)),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: result,
|
||||
),
|
||||
Container(
|
||||
height: entity.rowPadding,
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
width: 0.0,
|
||||
height: 0.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_resetTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SelectControlWidget extends StatefulWidget {
|
||||
|
||||
SelectControlWidget({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SelectControlWidgetState createState() => _SelectControlWidgetState();
|
||||
}
|
||||
|
||||
class _SelectControlWidgetState extends State<SelectControlWidget> {
|
||||
|
||||
void setNewState(domain, entityId, newValue) {
|
||||
eventBus.fire(new ServiceCallEvent(domain, "select_option", entityId,
|
||||
{"option": "$newValue"}));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final SelectEntity entity = entityModel.entity;
|
||||
Widget ctrl;
|
||||
if (entity.listOptions.isNotEmpty) {
|
||||
ctrl = DropdownButton<String>(
|
||||
value: entity.state,
|
||||
items: entity.listOptions.map((String value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: new Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (_) {
|
||||
setNewState(entity.domain, entity.entityId,_);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
ctrl = Text('---');
|
||||
}
|
||||
return Expanded(
|
||||
//width: Entity.INPUT_WIDTH,
|
||||
child: ctrl,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class CoverControlWidget extends StatefulWidget {
|
||||
|
||||
CoverControlWidget({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CoverControlWidgetState createState() => _CoverControlWidgetState();
|
||||
}
|
||||
|
||||
class _CoverControlWidgetState extends State<CoverControlWidget> {
|
||||
|
||||
double _tmpPosition = 0.0;
|
||||
double _tmpTiltPosition = 0.0;
|
||||
bool _changedHere = false;
|
||||
|
||||
void _setNewPosition(CoverEntity entity, double position) {
|
||||
setState(() {
|
||||
_tmpPosition = position.roundToDouble();
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()}));
|
||||
});
|
||||
}
|
||||
|
||||
void _setNewTiltPosition(CoverEntity entity, double position) {
|
||||
setState(() {
|
||||
_tmpTiltPosition = position.roundToDouble();
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()}));
|
||||
});
|
||||
}
|
||||
|
||||
void _resetVars(CoverEntity entity) {
|
||||
_tmpPosition = entity.currentPosition;
|
||||
_tmpTiltPosition = entity.currentTiltPosition;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final CoverEntity entity = entityModel.entity;
|
||||
if (_changedHere) {
|
||||
_changedHere = false;
|
||||
} else {
|
||||
_resetVars(entity);
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(entity.leftWidgetPadding, entity.rowPadding, entity.rightWidgetPadding, 0.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
_buildPositionControls(entity),
|
||||
_buildTiltControls(entity)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPositionControls(CoverEntity entity) {
|
||||
if (entity.supportSetPosition) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
0.0, entity.rowPadding, 0.0, entity.rowPadding),
|
||||
child: Text("Position", style: TextStyle(
|
||||
fontSize: entity.stateFontSize
|
||||
)),
|
||||
),
|
||||
Slider(
|
||||
value: _tmpPosition,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
divisions: 10,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_tmpPosition = value.roundToDouble();
|
||||
_changedHere = true;
|
||||
});
|
||||
},
|
||||
onChangeEnd: (double value) => _setNewPosition(entity, value),
|
||||
),
|
||||
Container(height: entity.rowPadding,)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Container(width: 0.0, height: 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTiltControls(CoverEntity entity) {
|
||||
List<Widget> controls = [];
|
||||
if (entity.supportCloseTilt || entity.supportOpenTilt || entity.supportStopTilt) {
|
||||
controls.add(
|
||||
CoverEntityTiltControlState()
|
||||
);
|
||||
}
|
||||
if (entity.supportSetTiltPosition) {
|
||||
controls.addAll(<Widget>[
|
||||
Slider(
|
||||
value: _tmpTiltPosition,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
divisions: 10,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_tmpTiltPosition = value.roundToDouble();
|
||||
_changedHere = true;
|
||||
});
|
||||
},
|
||||
onChangeEnd: (double value) => _setNewTiltPosition(entity, value),
|
||||
),
|
||||
Container(height: entity.rowPadding,)
|
||||
]);
|
||||
}
|
||||
if (controls.isNotEmpty) {
|
||||
controls.insert(0, Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
0.0, entity.rowPadding, 0.0, entity.rowPadding),
|
||||
child: Text("Tilt position", style: TextStyle(
|
||||
fontSize: entity.stateFontSize
|
||||
)),
|
||||
));
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: controls,
|
||||
);
|
||||
} else {
|
||||
return Container(width: 0.0, height: 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
569
lib/entity_class/stateless_widgets.dart
Normal file
@ -0,0 +1,569 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class EntityWidgetsSizes {
|
||||
|
||||
}
|
||||
|
||||
class EntityModel extends InheritedWidget {
|
||||
|
||||
const EntityModel({
|
||||
Key key,
|
||||
@required this.entity,
|
||||
@required this.handleTap,
|
||||
@required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final Entity entity;
|
||||
final bool handleTap;
|
||||
|
||||
static EntityModel of(BuildContext context) {
|
||||
return context.inheritFromWidgetOfExactType(EntityModel);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedWidget oldWidget) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DefaultEntityContainer extends StatelessWidget {
|
||||
|
||||
DefaultEntityContainer({
|
||||
Key key,
|
||||
@required this.state,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget state;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
return SizedBox(
|
||||
height: entityModel.entity.widgetHeight,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
EntityIcon(),
|
||||
Expanded(
|
||||
child: EntityName(),
|
||||
),
|
||||
state
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EntityPageContainer extends StatelessWidget {
|
||||
|
||||
EntityPageContainer({Key key, @required this.children}) : super(key: key);
|
||||
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SimpleEntityState extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
return Padding(
|
||||
padding:
|
||||
EdgeInsets.fromLTRB(0.0, 0.0, entityModel.entity.rightWidgetPadding, 0.0),
|
||||
child: GestureDetector(
|
||||
child: Text(
|
||||
"${entityModel.entity.state}${entityModel.entity.unitOfMeasurement}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: entityModel.entity.stateFontSize,
|
||||
)),
|
||||
onTap: () => entityModel.handleTap ? eventBus.fire(new ShowEntityPageEvent(entityModel.entity)) : null,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EntityName extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
return GestureDetector(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 10.0),
|
||||
child: Text(
|
||||
"${entityModel.entity.displayName}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
style: TextStyle(fontSize: entityModel.entity.nameFontSize),
|
||||
),
|
||||
),
|
||||
onTap: () => entityModel.handleTap ? eventBus.fire(new ShowEntityPageEvent(entityModel.entity)) : null,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EntityIcon extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
return GestureDetector(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(entityModel.entity.leftWidgetPadding, 0.0, 12.0, 0.0),
|
||||
//TODO: move createIconWidgetFromEntityData into this widget
|
||||
child: MaterialDesignIcons.createIconWidgetFromEntityData(
|
||||
entityModel.entity,
|
||||
entityModel.entity.iconSize,
|
||||
Entity.STATE_ICONS_COLORS[entityModel.entity.state] ?? Entity.STATE_ICONS_COLORS["default"]),
|
||||
),
|
||||
onTap: () => entityModel.handleTap ? eventBus.fire(new ShowEntityPageEvent(entityModel.entity)) : null,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class LastUpdatedWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
entityModel.entity.leftWidgetPadding, 0.0, 0.0, 0.0),
|
||||
child: Text(
|
||||
'${entityModel.entity.lastUpdated}',
|
||||
textAlign: TextAlign.left,
|
||||
style:
|
||||
TextStyle(fontSize: entityModel.entity.smallFontSize, color: Colors.black26),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EntityAttributesList extends StatelessWidget {
|
||||
|
||||
EntityAttributesList({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
List<Widget> attrs = [];
|
||||
if ((entityModel.entity.attributesToShow == null) || (entityModel.entity.attributesToShow.contains("all"))) {
|
||||
entityModel.entity.attributes.forEach((name, value){
|
||||
attrs.add(
|
||||
_buildSingleAttribute(entityModel.entity, "$name", "$value")
|
||||
);
|
||||
});
|
||||
} else {
|
||||
entityModel.entity.attributesToShow.forEach((String attr) {
|
||||
String attrValue = entityModel.entity.getAttribute("$attr");
|
||||
if (attrValue != null) {
|
||||
attrs.add(
|
||||
_buildSingleAttribute(entityModel.entity, "$attr", "$attrValue")
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
return Column(
|
||||
children: attrs,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSingleAttribute(Entity entity, String name, String value) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(entity.leftWidgetPadding, entity.rowPadding, 0.0, 0.0),
|
||||
child: Text(
|
||||
"$name",
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, entity.rowPadding, entity.rightWidgetPadding, 0.0),
|
||||
child: Text(
|
||||
"$value",
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Badge extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
double iconSize = 26.0;
|
||||
Widget badgeIcon;
|
||||
String onBadgeTextValue;
|
||||
Color iconColor = Entity.badgeColors[entityModel.entity.domain] ?? Entity.badgeColors["default"];
|
||||
switch (entityModel.entity.domain) {
|
||||
case "sun": {
|
||||
badgeIcon = entityModel.entity.state == "below_horizon" ?
|
||||
Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
|
||||
size: iconSize,
|
||||
) :
|
||||
Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8),
|
||||
size: iconSize,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "sensor": {
|
||||
onBadgeTextValue = entityModel.entity.unitOfMeasurement;
|
||||
badgeIcon = Center(
|
||||
child: Text(
|
||||
"${entityModel.entity.state}",
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 17.0),
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "device_tracker": {
|
||||
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(entityModel.entity, iconSize,Colors.black);
|
||||
onBadgeTextValue = entityModel.entity.state;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(entityModel.entity, iconSize,Colors.black);
|
||||
}
|
||||
}
|
||||
Widget onBadgeText;
|
||||
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
|
||||
onBadgeText = Container(width: 0.0, height: 0.0);
|
||||
} else {
|
||||
onBadgeText = Container(
|
||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||
child: Text("$onBadgeTextValue",
|
||||
style: TextStyle(fontSize: 12.0, color: Colors.white),
|
||||
textAlign: TextAlign.center, softWrap: false, overflow: TextOverflow.fade),
|
||||
decoration: new BoxDecoration(
|
||||
// Circle shape
|
||||
//shape: BoxShape.circle,
|
||||
color: iconColor,
|
||||
borderRadius: BorderRadius.circular(9.0),
|
||||
)
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
|
||||
width: 50.0,
|
||||
height: 50.0,
|
||||
decoration: new BoxDecoration(
|
||||
// Circle shape
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
// The border you want
|
||||
border: new Border.all(
|
||||
width: 2.0,
|
||||
color: iconColor,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
overflow: Overflow.visible,
|
||||
children: <Widget>[
|
||||
Positioned(
|
||||
width: 46.0,
|
||||
height: 46.0,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
child: badgeIcon,
|
||||
),
|
||||
Positioned(
|
||||
//width: 50.0,
|
||||
bottom: -9.0,
|
||||
left: -10.0,
|
||||
right: -10.0,
|
||||
child: Center(
|
||||
child: onBadgeText,
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 60.0,
|
||||
child: Text(
|
||||
"${entityModel.entity.displayName}",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
softWrap: true,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => eventBus.fire(new ShowEntityPageEvent(entityModel.entity))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ClimateStateWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final ClimateEntity entity = entityModel.entity;
|
||||
return Padding(
|
||||
padding:
|
||||
EdgeInsets.fromLTRB(0.0, 0.0, entityModel.entity.rightWidgetPadding, 0.0),
|
||||
child: GestureDetector(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"${entity.state}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: entityModel.entity.stateFontSize,
|
||||
)),
|
||||
Text(
|
||||
entity.supportTargetTemperature ? " ${entity.temperature}" : " ${entity.targetLow} - ${entity.targetHigh}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: entityModel.entity.stateFontSize,
|
||||
))
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"Currently: ${entity.attributes["current_temperature"]}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: entityModel.entity.stateFontSize,
|
||||
color: Colors.black45
|
||||
))
|
||||
],
|
||||
),
|
||||
onTap: () => entityModel.handleTap ? eventBus.fire(new ShowEntityPageEvent(entity)) : null,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DateTimeStateWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final DateTimeEntity entity = entityModel.entity;
|
||||
return Padding(
|
||||
padding:
|
||||
EdgeInsets.fromLTRB(0.0, 0.0, entity.rightWidgetPadding, 0.0),
|
||||
child: GestureDetector(
|
||||
child: Text(
|
||||
"${entity.formattedState}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: entity.stateFontSize,
|
||||
)),
|
||||
onTap: () => _handleStateTap(context, entity),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _handleStateTap(BuildContext context, DateTimeEntity entity) {
|
||||
if (entity.hasDate) {
|
||||
_showDatePicker(context, entity).then((date) {
|
||||
if (date != null) {
|
||||
if (entity.hasTime) {
|
||||
_showTimePicker(context, entity).then((time){
|
||||
entity.setNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}", "time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
|
||||
});
|
||||
} else {
|
||||
entity.setNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}"});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (entity.hasTime) {
|
||||
_showTimePicker(context, entity).then((time){
|
||||
if (time != null) {
|
||||
entity.setNewState({"time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
TheLogger.log("Warning", "${entity.entityId} has no date and no time");
|
||||
}
|
||||
}
|
||||
|
||||
Future _showDatePicker(BuildContext context, DateTimeEntity entity) {
|
||||
return showDatePicker(
|
||||
context: context,
|
||||
initialDate: entity.dateTimeState,
|
||||
firstDate: DateTime(1970),
|
||||
lastDate: DateTime(2037) //Unix timestamp will finish at Jan 19, 2038
|
||||
);
|
||||
}
|
||||
|
||||
Future _showTimePicker(BuildContext context, DateTimeEntity entity) {
|
||||
return showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(entity.dateTimeState)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CoverEntityControlState extends StatelessWidget {
|
||||
|
||||
void _open(CoverEntity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "open_cover", entity.entityId, null));
|
||||
}
|
||||
|
||||
void _close(CoverEntity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "close_cover", entity.entityId, null));
|
||||
}
|
||||
|
||||
void _stop(CoverEntity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "stop_cover", entity.entityId, null));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final CoverEntity entity = entityModel.entity;
|
||||
List<Widget> buttons = [];
|
||||
if (entity.supportOpen) {
|
||||
buttons.add(
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-up"),
|
||||
size: entity.iconSize,
|
||||
),
|
||||
onPressed: entity.canBeOpened ? () =>_open(entity) : null
|
||||
)
|
||||
);
|
||||
} else {
|
||||
buttons.add(Container(width: entity.iconSize+20.0,));
|
||||
}
|
||||
if (entity.supportStop) {
|
||||
buttons.add(
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
|
||||
size: entity.iconSize,
|
||||
),
|
||||
onPressed: () => _stop(entity)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
buttons.add(Container(width: entity.iconSize+20.0,));
|
||||
}
|
||||
if (entity.supportClose) {
|
||||
buttons.add(
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-down"),
|
||||
size: entity.iconSize,
|
||||
),
|
||||
onPressed: entity.canBeClosed ? () => _close(entity) : null
|
||||
)
|
||||
);
|
||||
} else {
|
||||
buttons.add(Container(width: entity.iconSize+20.0,));
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: buttons,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CoverEntityTiltControlState extends StatelessWidget {
|
||||
|
||||
void _open(CoverEntity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "open_cover_tilt", entity.entityId, null));
|
||||
}
|
||||
|
||||
void _close(CoverEntity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "close_cover_tilt", entity.entityId, null));
|
||||
}
|
||||
|
||||
void _stop(CoverEntity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "stop_cover_tilt", entity.entityId, null));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final CoverEntity entity = entityModel.entity;
|
||||
List<Widget> buttons = [];
|
||||
if (entity.supportOpenTilt) {
|
||||
buttons.add(
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-top-right"),
|
||||
size: entity.iconSize,
|
||||
),
|
||||
onPressed: entity.canTiltBeOpened ? () =>_open(entity) : null
|
||||
)
|
||||
);
|
||||
} else {
|
||||
buttons.add(Container(width: entity.iconSize+20.0,));
|
||||
}
|
||||
if (entity.supportStopTilt) {
|
||||
buttons.add(
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
|
||||
size: entity.iconSize,
|
||||
),
|
||||
onPressed: () => _stop(entity)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
buttons.add(Container(width: entity.iconSize+20.0,));
|
||||
}
|
||||
if (entity.supportCloseTilt) {
|
||||
buttons.add(
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-bottom-left"),
|
||||
size: entity.iconSize,
|
||||
),
|
||||
onPressed: entity.canTiltBeClosed ? () => _close(entity) : null
|
||||
)
|
||||
);
|
||||
} else {
|
||||
buttons.add(Container(width: entity.iconSize+20.0,));
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: buttons,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class _SwitchEntityWidgetState extends _EntityWidgetState {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void setNewState(newValue) {
|
||||
setState(() {
|
||||
widget.entity.assumedState = newValue ? 'on' : 'off';
|
||||
});
|
||||
Timer(Duration(seconds: 2), (){
|
||||
setState(() {
|
||||
widget.entity.assumedState = widget.entity.state;
|
||||
});
|
||||
});
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
widget.entity.domain, (newValue as bool) ? "turn_on" : "turn_off", widget.entity.entityId, null));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||
return Switch(
|
||||
value: widget.entity.assumedState == 'on',
|
||||
onChanged: ((switchState) {
|
||||
setNewState(switchState);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class _TextEntityWidgetState extends _EntityWidgetState {
|
||||
String _tmpValue;
|
||||
FocusNode _focusNode = FocusNode();
|
||||
bool validValue = false;
|
||||
|
||||
int get valueMinLength => widget.entity._attributes["min"] ?? -1;
|
||||
int get valueMaxLength => widget.entity._attributes["max"] ?? -1;
|
||||
String get valuePattern => widget.entity._attributes["pattern"] ?? null;
|
||||
bool get isTextField => widget.entity._attributes["mode"] == "text";
|
||||
bool get isPasswordField => widget.entity._attributes["mode"] == "password";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode.addListener(_focusListener);
|
||||
_tmpValue = widget.entity.state;
|
||||
}
|
||||
|
||||
@override
|
||||
void setNewState(newValue) {
|
||||
if (validate(newValue)) {
|
||||
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_value", widget.entity.entityId,
|
||||
{"value": "$newValue"}));
|
||||
} else {
|
||||
setState(() {
|
||||
_tmpValue = widget.entity.state;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool validate(newValue) {
|
||||
if (newValue is String) {
|
||||
validValue = (newValue.length >= this.valueMinLength) &&
|
||||
(this.valueMaxLength == -1 ||
|
||||
(newValue.length <= this.valueMaxLength));
|
||||
} else {
|
||||
validValue = true;
|
||||
}
|
||||
return validValue;
|
||||
}
|
||||
|
||||
void _focusListener() {
|
||||
if (!_focusNode.hasFocus && (_tmpValue != widget.entity.state)) {
|
||||
setNewState(_tmpValue);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
||||
if (!_focusNode.hasFocus && (_tmpValue != widget.entity.state)) {
|
||||
_tmpValue = widget.entity.state;
|
||||
}
|
||||
if (this.isTextField || this.isPasswordField) {
|
||||
return Expanded(
|
||||
//width: Entity.INPUT_WIDTH,
|
||||
child: TextField(
|
||||
focusNode: _focusNode,
|
||||
obscureText: this.isPasswordField,
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _tmpValue,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _tmpValue.length))),
|
||||
onChanged: (value) {
|
||||
_tmpValue = value;
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
TheLogger.log("Warning", "Unsupported input mode for ${widget.entity.entityId}");
|
||||
return super._buildActionWidget(inCard, context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(_focusListener);
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
@ -5,6 +5,8 @@ class EntityCollection {
|
||||
Map<String, Entity> _entities;
|
||||
List<String> viewList;
|
||||
|
||||
bool get isEmpty => _entities.isEmpty;
|
||||
|
||||
EntityCollection() {
|
||||
_entities = {};
|
||||
viewList = [];
|
||||
@ -27,7 +29,42 @@ class EntityCollection {
|
||||
}
|
||||
|
||||
Entity _createEntityInstance(rawEntityData) {
|
||||
return Entity(rawEntityData);
|
||||
switch (rawEntityData["entity_id"].split(".")[0]) {
|
||||
case 'sun': {
|
||||
return SunEntity(rawEntityData);
|
||||
}
|
||||
case "automation":
|
||||
case "input_boolean":
|
||||
case "switch":
|
||||
case "light": {
|
||||
return SwitchEntity(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) {
|
||||
@ -49,14 +86,24 @@ class EntityCollection {
|
||||
}
|
||||
|
||||
void updateFromRaw(Map rawEntityData) {
|
||||
//TODO pass entity in this function and call update from it
|
||||
_entities[rawEntityData["entity_id"]].update(rawEntityData);
|
||||
get("${rawEntityData["entity_id"]}")?.update(rawEntityData);
|
||||
}
|
||||
|
||||
Entity get(String entityId) {
|
||||
return _entities[entityId];
|
||||
}
|
||||
|
||||
List<Entity> getAll(List ids) {
|
||||
List<Entity> result = [];
|
||||
ids.forEach((id){
|
||||
Entity en = get(id);
|
||||
if (en != null) {
|
||||
result.add(en);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isExist(String entityId) {
|
||||
return _entities[entityId] != null;
|
||||
}
|
||||
@ -76,7 +123,7 @@ class EntityCollection {
|
||||
entities.forEach((entiyId) {
|
||||
bool foundInGroup = false;
|
||||
result["userGroups"].forEach((userGroupId) {
|
||||
if (_entities[userGroupId].childEntities.contains(entiyId)) {
|
||||
if (_entities[userGroupId].childEntityIds.contains(entiyId)) {
|
||||
foundInGroup = true;
|
||||
}
|
||||
});
|
||||
|
@ -1,9 +1,9 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class HomeAssistant {
|
||||
String _hassioAPIEndpoint;
|
||||
String _hassioPassword;
|
||||
String _hassioAuthType;
|
||||
String _webSocketAPIEndpoint;
|
||||
String _password;
|
||||
String _authType;
|
||||
|
||||
IOWebSocketChannel _hassioChannel;
|
||||
SendMessageQueue _messageQueue;
|
||||
@ -13,40 +13,44 @@ class HomeAssistant {
|
||||
int _servicesMessageId = 0;
|
||||
int _subscriptionMessageId = 0;
|
||||
int _configMessageId = 0;
|
||||
int _userInfoMessageId = 0;
|
||||
EntityCollection _entities;
|
||||
UIBuilder _uiBuilder;
|
||||
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 = 50; //seconds
|
||||
Duration fetchTimeout = Duration(seconds: 45);
|
||||
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.viewList.length ?? 0;
|
||||
UIBuilder get uiBuilder => _uiBuilder;
|
||||
|
||||
EntityCollection get entities => _entities;
|
||||
|
||||
HomeAssistant() {
|
||||
_entities = EntityCollection();
|
||||
_uiBuilder = UIBuilder();
|
||||
_messageQueue = SendMessageQueue(messageExpirationTime);
|
||||
}
|
||||
|
||||
void updateConnectionSettings(String url, String password, String authType) {
|
||||
_hassioAPIEndpoint = url;
|
||||
_hassioPassword = password;
|
||||
_hassioAuthType = authType;
|
||||
_webSocketAPIEndpoint = url;
|
||||
_password = password;
|
||||
_authType = authType;
|
||||
}
|
||||
|
||||
Future fetch() {
|
||||
@ -55,113 +59,147 @@ class HomeAssistant {
|
||||
} else {
|
||||
_fetchCompleter = new Completer();
|
||||
_fetchTimer = Timer(fetchTimeout, () {
|
||||
closeConnection();
|
||||
TheLogger.log("Error", "Data fetching timeout");
|
||||
_finishFetching({"errorCode" : 9,"errorMessage": "Couldn't get data from server"});
|
||||
disconnect().then((_) {
|
||||
_completeFetching({
|
||||
"errorCode": 9,
|
||||
"errorMessage": "Couldn't get data from server"
|
||||
});
|
||||
});
|
||||
});
|
||||
_connection().then((r) {
|
||||
_getData();
|
||||
}).catchError((e) {
|
||||
_finishFetching(e);
|
||||
_completeFetching(e);
|
||||
});
|
||||
}
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
|
||||
closeConnection() {
|
||||
if (_socketSubscription != null) {
|
||||
_socketSubscription.cancel();
|
||||
}
|
||||
if (_hassioChannel != null) {
|
||||
if (_hassioChannel.closeCode == null) {
|
||||
_hassioChannel.sink?.close();
|
||||
}
|
||||
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.sink == null) || (_hassioChannel.closeCode != null)) {
|
||||
closeConnection();
|
||||
TheLogger.log("Debug", "Socket connecting...");
|
||||
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
|
||||
_connectionCompleter = new Completer();
|
||||
_connectionTimer = Timer(connectTimeout, () {
|
||||
closeConnection();
|
||||
TheLogger.log("Error", "Socket connection timeout");
|
||||
_finishConnecting({"errorCode" : 1,"errorMessage": "Couldn't connect to Home Assistant. Looks like a network issues"});
|
||||
});
|
||||
_hassioChannel = IOWebSocketChannel.connect(
|
||||
_hassioAPIEndpoint, pingInterval: Duration(seconds: 30));
|
||||
_hassioChannel.stream.handleError((e) {
|
||||
TheLogger.log("Error", "Unhandled socket error: ${e.toString()}");
|
||||
});
|
||||
if (_socketSubscription != null) _socketSubscription.cancel();
|
||||
_socketSubscription = _hassioChannel.stream.listen((message) =>
|
||||
_handleMessage(_connectionCompleter, message));
|
||||
_hassioChannel.sink.done.whenComplete(() {
|
||||
TheLogger.log("Debug","Socket sink finished. Assuming it is closed.");
|
||||
closeConnection();
|
||||
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 {
|
||||
//TheLogger.log("Debug","Socket looks connected...${_hassioChannel.protocol}, ${_hassioChannel.closeCode}, ${_hassioChannel.closeReason}");
|
||||
_finishConnecting(null);
|
||||
_completeConnecting(null);
|
||||
}
|
||||
}
|
||||
return _connectionCompleter.future;
|
||||
}
|
||||
|
||||
_getData() {
|
||||
_getConfig().then((result) {
|
||||
_getStates().then((result) {
|
||||
_getServices().then((result) {
|
||||
_finishFetching(null);
|
||||
}).catchError((e) {
|
||||
_finishFetching(e);
|
||||
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."
|
||||
});
|
||||
}).catchError((e) {
|
||||
_finishFetching(e);
|
||||
});
|
||||
}).catchError((e) {
|
||||
_finishFetching(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _reconnect() {
|
||||
disconnect().then((_) {
|
||||
_connection().catchError((e){
|
||||
_completeConnecting(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _finishFetching(error) {
|
||||
_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();
|
||||
_finishConnecting(error);
|
||||
if (error != null) {
|
||||
if (!_fetchCompleter.isCompleted)
|
||||
_completeConnecting(error);
|
||||
if (!_fetchCompleter.isCompleted) {
|
||||
if (error != null) {
|
||||
_fetchCompleter.completeError(error);
|
||||
} else {
|
||||
if (!_fetchCompleter.isCompleted)
|
||||
} else {
|
||||
autoReconnect = true;
|
||||
TheLogger.log("Debug", "Fetch complete successful");
|
||||
_fetchCompleter.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _finishConnecting(error) {
|
||||
void _completeConnecting(error) {
|
||||
_connectionTimer.cancel();
|
||||
if (error != null) {
|
||||
if (!_connectionCompleter.isCompleted)
|
||||
if (!_connectionCompleter.isCompleted) {
|
||||
if (error != null) {
|
||||
_connectionCompleter.completeError(error);
|
||||
} else {
|
||||
if (!_connectionCompleter.isCompleted)
|
||||
} else {
|
||||
_connectionCompleter.complete();
|
||||
}
|
||||
} else if (error != null) {
|
||||
eventBus.fire(ShowErrorEvent(error["errorMessage"], error["errorCode"]));
|
||||
}
|
||||
}
|
||||
|
||||
_handleMessage(Completer connectionCompleter, String message) {
|
||||
_handleMessage(String message) {
|
||||
var data = json.decode(message);
|
||||
//TheLogger.log("Debug","[Received] => Message type: ${data['type']}");
|
||||
TheLogger.log("Debug","[Received] => ${data['type']}");
|
||||
if (data["type"] == "auth_required") {
|
||||
_sendAuthMessageRaw('{"type": "auth","$_hassioAuthType": "$_hassioPassword"}');
|
||||
_sendAuthMessageRaw('{"type": "auth","$_authType": "$_password"}');
|
||||
} else if (data["type"] == "auth_ok") {
|
||||
_finishConnecting(null);
|
||||
_completeConnecting(null);
|
||||
_sendSubscribe();
|
||||
} else if (data["type"] == "auth_invalid") {
|
||||
_finishFetching({"errorCode": 6, "errorMessage": "${data["message"]}"});
|
||||
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
|
||||
} else if (data["type"] == "result") {
|
||||
if (data["id"] == _configMessageId) {
|
||||
_parseConfig(data);
|
||||
@ -169,6 +207,8 @@ class HomeAssistant {
|
||||
_parseEntities(data);
|
||||
} else if (data["id"] == _servicesMessageId) {
|
||||
_parseServices(data);
|
||||
} else if (data["id"] == _userInfoMessageId) {
|
||||
_parseUserInfo(data);
|
||||
} else if (data["id"] == _currentMessageId) {
|
||||
TheLogger.log("Debug","Request id:$_currentMessageId was successful");
|
||||
}
|
||||
@ -209,6 +249,15 @@ class HomeAssistant {
|
||||
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();
|
||||
@ -246,12 +295,16 @@ class HomeAssistant {
|
||||
return sendCompleter.future;
|
||||
}
|
||||
|
||||
Future callService(String domain, String service, String entityId, Map<String, String> additionalParams) {
|
||||
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){
|
||||
message += ', "$name" : "$value"';
|
||||
if ((value is double) || (value is int)) {
|
||||
message += ', "$name" : $value';
|
||||
} else {
|
||||
message += ', "$name" : "$value"';
|
||||
}
|
||||
});
|
||||
}
|
||||
message += '}}';
|
||||
@ -260,8 +313,9 @@ class HomeAssistant {
|
||||
|
||||
void _handleEntityStateChange(Map eventData) {
|
||||
//TheLogger.log("Debug", "New state for ${eventData['entity_id']}");
|
||||
_entities.updateState(eventData);
|
||||
eventBus.fire(new StateChangedEvent(eventData["entity_id"], null, false));
|
||||
Map data = Map.from(eventData);
|
||||
_entities.updateState(data);
|
||||
eventBus.fire(new StateChangedEvent(data["entity_id"], null, false));
|
||||
}
|
||||
|
||||
void _parseConfig(Map data) {
|
||||
@ -273,6 +327,15 @@ class HomeAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
void _parseUserInfo(Map data) {
|
||||
if (data["success"] == true) {
|
||||
_userName = data["result"]["name"];
|
||||
} else {
|
||||
_userName = null;
|
||||
}
|
||||
_userInfoCompleter.complete();
|
||||
}
|
||||
|
||||
void _parseServices(response) {
|
||||
_servicesCompleter.complete();
|
||||
/*if (response["success"] == false) {
|
||||
@ -305,9 +368,41 @@ class HomeAssistant {
|
||||
return;
|
||||
}
|
||||
_entities.parse(response["result"]);
|
||||
_uiBuilder.build(_entities);
|
||||
_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 {
|
||||
@ -346,6 +441,6 @@ class HAMessage {
|
||||
}
|
||||
|
||||
bool isExpired() {
|
||||
return _timeStamp.difference(DateTime.now()).inSeconds > _messageTimeout;
|
||||
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
|
||||
}
|
||||
}
|
516
lib/main.dart
@ -11,14 +11,11 @@ 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;
|
||||
|
||||
part 'entity_class/entity.class.dart';
|
||||
part 'entity_class/button_entity.class.dart';
|
||||
part 'entity_class/datetime_entity.class.dart';
|
||||
part 'entity_class/select_entity.class.dart';
|
||||
part 'entity_class/slider_entity.class.dart';
|
||||
part 'entity_class/switch_entity.class.dart';
|
||||
part 'entity_class/text_entity.class.dart';
|
||||
part 'entity_class/stateless_widgets.dart';
|
||||
part 'entity_class/stateful_widgets.dart';
|
||||
|
||||
part 'settings.page.dart';
|
||||
part 'home_assistant.class.dart';
|
||||
@ -27,14 +24,13 @@ part 'entity.page.dart';
|
||||
part 'utils.class.dart';
|
||||
part 'mdi.class.dart';
|
||||
part 'entity_collection.class.dart';
|
||||
part 'ui_builder_class.dart';
|
||||
part 'view_builder.class.dart';
|
||||
part 'view_class.dart';
|
||||
part 'card_class.dart';
|
||||
part 'badge_class.dart';
|
||||
|
||||
EventBus eventBus = new EventBus();
|
||||
const String appName = "HA Client";
|
||||
const appVersion = "0.2.4";
|
||||
const appVersion = "0.3.0.38";
|
||||
|
||||
String homeAssistantWebHost;
|
||||
|
||||
@ -88,27 +84,25 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
HomeAssistant _homeAssistant;
|
||||
EntityCollection _entities;
|
||||
//Map _instanceConfig;
|
||||
String _apiEndpoint;
|
||||
String _apiPassword;
|
||||
String _webSocketApiEndpoint;
|
||||
String _password;
|
||||
String _authType;
|
||||
int _uiViewsCount = 0;
|
||||
String _instanceHost;
|
||||
int _errorCodeToBeShown = 0;
|
||||
String _lastErrorMessage = "";
|
||||
StreamSubscription _stateSubscription;
|
||||
StreamSubscription _settingsSubscription;
|
||||
StreamSubscription _serviceCallSubscription;
|
||||
StreamSubscription _showEntityPageSubscription;
|
||||
bool _isLoading = true;
|
||||
|
||||
Map<String, Color> _badgeColors = {
|
||||
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
||||
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
||||
};
|
||||
StreamSubscription _refreshDataSubscription;
|
||||
StreamSubscription _showErrorSubscription;
|
||||
int _isLoading = 1;
|
||||
bool _settingsLoaded = false;
|
||||
bool _accountMenuExpanded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_settingsLoaded = false;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
_homeAssistant = HomeAssistant();
|
||||
@ -116,56 +110,52 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
|
||||
TheLogger.log("Debug","Settings change event: reconnect=${event.reconnect}");
|
||||
if (event.reconnect) {
|
||||
_homeAssistant.closeConnection();
|
||||
_initConnection().then((b){
|
||||
setState(() {
|
||||
_homeAssistant.updateConnectionSettings(_apiEndpoint, _apiPassword, _authType);
|
||||
_errorCodeToBeShown = 10;
|
||||
_lastErrorMessage = "Connection settings was changed.";
|
||||
});
|
||||
}, onError: (_) {
|
||||
setState(() {
|
||||
_lastErrorMessage = _;
|
||||
_errorCodeToBeShown = 5;
|
||||
});
|
||||
});
|
||||
_homeAssistant.disconnect().then((_){
|
||||
_initialLoad();
|
||||
});
|
||||
}
|
||||
});
|
||||
_initConnection().then((_){
|
||||
_createConnection();
|
||||
_initialLoad();
|
||||
}
|
||||
|
||||
void _initialLoad() {
|
||||
_loadConnectionSettings().then((_){
|
||||
_subscribe();
|
||||
_refreshData();
|
||||
}, onError: (_) {
|
||||
setState(() {
|
||||
_lastErrorMessage = _;
|
||||
_errorCodeToBeShown = 5;
|
||||
_isLoading = 2;
|
||||
});
|
||||
_showErrorSnackBar(message: _, errorCode: 5);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
TheLogger.log("Debug","$state");
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (state == AppLifecycleState.resumed && _settingsLoaded) {
|
||||
_refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
_initConnection() async {
|
||||
_loadConnectionSettings() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
String domain = prefs.getString('hassio-domain');
|
||||
String port = prefs.getString('hassio-port');
|
||||
_instanceHost = "$domain:$port";
|
||||
_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";
|
||||
_apiPassword = prefs.getString('hassio-password');
|
||||
_password = prefs.getString('hassio-password');
|
||||
_authType = prefs.getString('hassio-auth-type');
|
||||
if ((domain == null) || (port == null) || (_apiPassword == null) ||
|
||||
(domain.length == 0) || (port.length == 0) || (_apiPassword.length == 0)) {
|
||||
if ((domain == null) || (port == null) || (_password == null) ||
|
||||
(domain.length == 0) || (port.length == 0) || (_password.length == 0)) {
|
||||
throw("Check connection settings");
|
||||
} else {
|
||||
_settingsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
_createConnection() {
|
||||
_refreshData();
|
||||
_subscribe() {
|
||||
if (_stateSubscription == null) {
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
setState(() {
|
||||
@ -191,36 +181,50 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
_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 {
|
||||
_homeAssistant.updateConnectionSettings(_apiEndpoint, _apiPassword, _authType);
|
||||
_homeAssistant.updateConnectionSettings(_webSocketApiEndpoint, _password, _authType);
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_hideErrorSnackBar();
|
||||
_isLoading = 1;
|
||||
});
|
||||
_errorCodeToBeShown = 0;
|
||||
_lastErrorMessage = "";
|
||||
await _homeAssistant.fetch().then((result) {
|
||||
setState(() {
|
||||
//_instanceConfig = _homeAssistant.instanceConfig;
|
||||
_entities = _homeAssistant.entities;
|
||||
_uiViewsCount = _homeAssistant.viewsCount;
|
||||
_isLoading = false;
|
||||
_isLoading = 0;
|
||||
});
|
||||
}).catchError((e) {
|
||||
_setErrorState(e);
|
||||
});
|
||||
eventBus.fire(RefreshDataFinishedEvent());
|
||||
}
|
||||
|
||||
_setErrorState(e) {
|
||||
setState(() {
|
||||
_errorCodeToBeShown = e["errorCode"] != null ? e["errorCode"] : 99;
|
||||
_lastErrorMessage = e["errorMessage"] ?? "Unknown error";
|
||||
_isLoading = false;
|
||||
_isLoading = 2;
|
||||
});
|
||||
_showErrorSnackBar(
|
||||
message: e != null ? e["errorMessage"] ?? "$e" : "Unknown error",
|
||||
errorCode: e["errorCode"] != null ? e["errorCode"] : 99
|
||||
);
|
||||
}
|
||||
|
||||
void _callService(String domain, String service, String entityId, Map<String, String> additionalParams) {
|
||||
void _callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
|
||||
_homeAssistant.callService(domain, service, entityId, additionalParams).catchError((e) => _setErrorState(e));
|
||||
}
|
||||
|
||||
@ -228,222 +232,27 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EntityViewPage(entity: entity),
|
||||
builder: (context) => EntityViewPage(entity: entity, homeAssistant: _homeAssistant),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildViews() {
|
||||
List<Widget> result = [];
|
||||
if ((_entities != null) && (!_homeAssistant.uiBuilder.isEmpty)) {
|
||||
_homeAssistant.uiBuilder.views.forEach((viewId, view) {
|
||||
result.add(
|
||||
RefreshIndicator(
|
||||
color: Colors.amber,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: _buildSingleView(view),
|
||||
),
|
||||
onRefresh: () => _refreshData(),
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<Widget> _buildSingleView(View view) {
|
||||
List<Widget> result = [];
|
||||
if (view.isThereBadges) {
|
||||
result.add(
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10.0,
|
||||
runSpacing: 1.0,
|
||||
children: _buildBadges(view.badges),
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
view.cards.forEach((id, card) {
|
||||
if (card.entities.isNotEmpty) {
|
||||
result.add(_buildCard(card));
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
List<Widget> _buildBadges( Map<String, Badge> badges) {
|
||||
List<Widget> result = [];
|
||||
badges.forEach((id, badge) {
|
||||
var badgeEntity = _entities.get(id);
|
||||
if (badgeEntity != null) {
|
||||
result.add(
|
||||
_buildSingleBadge(badgeEntity)
|
||||
);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
Widget _buildSingleBadge(Entity data) {
|
||||
double iconSize = 26.0;
|
||||
Widget badgeIcon;
|
||||
String badgeTextValue;
|
||||
Color iconColor = _badgeColors[data.domain] ?? _badgeColors["default"];
|
||||
switch (data.domain) {
|
||||
case "sun": {
|
||||
badgeIcon = data.state == "below_horizon" ?
|
||||
Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
|
||||
size: iconSize,
|
||||
) :
|
||||
Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8),
|
||||
size: iconSize,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "sensor": {
|
||||
badgeTextValue = data.unitOfMeasurement;
|
||||
badgeIcon = Center(
|
||||
child: Text(
|
||||
"${data.state == 'unknown' ? '-' : data.state}",
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 17.0),
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "device_tracker": {
|
||||
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(data, iconSize,Colors.black);
|
||||
badgeTextValue = data.state;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(data, iconSize,Colors.black);
|
||||
}
|
||||
}
|
||||
Widget badgeText;
|
||||
if (badgeTextValue == null || badgeTextValue.length == 0) {
|
||||
badgeText = Container(width: 0.0, height: 0.0);
|
||||
} else {
|
||||
badgeText = Container(
|
||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||
child: Text("$badgeTextValue",
|
||||
style: TextStyle(fontSize: 12.0, color: Colors.white),
|
||||
textAlign: TextAlign.center, softWrap: false, overflow: TextOverflow.fade),
|
||||
decoration: new BoxDecoration(
|
||||
// Circle shape
|
||||
//shape: BoxShape.circle,
|
||||
color: iconColor,
|
||||
borderRadius: BorderRadius.circular(9.0),
|
||||
)
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
|
||||
width: 50.0,
|
||||
height: 50.0,
|
||||
decoration: new BoxDecoration(
|
||||
// Circle shape
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
// The border you want
|
||||
border: new Border.all(
|
||||
width: 2.0,
|
||||
color: iconColor,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
overflow: Overflow.visible,
|
||||
children: <Widget>[
|
||||
Positioned(
|
||||
width: 46.0,
|
||||
height: 46.0,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
child: badgeIcon,
|
||||
),
|
||||
Positioned(
|
||||
//width: 50.0,
|
||||
bottom: -9.0,
|
||||
left: -10.0,
|
||||
right: -10.0,
|
||||
child: Center(
|
||||
child: badgeText,
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 60.0,
|
||||
child: Text(
|
||||
"${data.displayName}",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
softWrap: true,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Card _buildCard(HACard card) {
|
||||
List<Widget> body = [];
|
||||
body.add(_buildCardHeader(card.friendlyName));
|
||||
body.addAll(_buildCardBody(card.entities));
|
||||
Card result = Card(
|
||||
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
Widget _buildCardHeader(String name) {
|
||||
var result;
|
||||
if (name.trim().length > 0) {
|
||||
result = new ListTile(
|
||||
//leading: const Icon(Icons.device_hub),
|
||||
//subtitle: Text(".."),
|
||||
//trailing: Text("${data["state"]}"),
|
||||
title: Text("$name",
|
||||
textAlign: TextAlign.left,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: 25.0)),
|
||||
);
|
||||
} else {
|
||||
result = new Container(width: 0.0, height: 0.0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<Widget> _buildCardBody(List ids) {
|
||||
List<Widget> entities = [];
|
||||
ids.forEach((id) {
|
||||
var entity = _entities.get(id);
|
||||
if (entity != null) {
|
||||
entities.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
|
||||
child: entity.buildWidget(context, true),
|
||||
));
|
||||
}
|
||||
});
|
||||
return entities;
|
||||
}
|
||||
|
||||
List<Tab> buildUIViewTabs() {
|
||||
//TODO move somewhere to ViewBuilder
|
||||
List<Tab> result = [];
|
||||
if ((_entities != null) && (!_homeAssistant.uiBuilder.isEmpty)) {
|
||||
_homeAssistant.uiBuilder.views.forEach((viewId, view) {
|
||||
if (!_entities.isEmpty) {
|
||||
if (!_entities.hasDefaultView) {
|
||||
result.add(
|
||||
Tab(
|
||||
icon:
|
||||
Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
|
||||
size: 24.0,
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
_entities.viewList.forEach((viewId) {
|
||||
result.add(
|
||||
Tab(
|
||||
icon: MaterialDesignIcons.createIconWidgetFromEntityData(_entities.get(viewId), 24.0, null) ??
|
||||
@ -462,7 +271,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
Row titleRow = Row(
|
||||
children: [Text(_homeAssistant != null ? _homeAssistant.locationName : "")],
|
||||
);
|
||||
if (_isLoading) {
|
||||
if (_isLoading == 1) {
|
||||
titleRow.children.add(Padding(
|
||||
child: JumpingDotsProgressIndicator(
|
||||
fontSize: 26.0,
|
||||
@ -470,77 +279,92 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
),
|
||||
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;
|
||||
}
|
||||
|
||||
Drawer _buildAppDrawer() {
|
||||
List<Widget> menuItems = [];
|
||||
menuItems.add(
|
||||
UserAccountsDrawerHeader(
|
||||
accountName: Text(_homeAssistant.userName),
|
||||
accountEmail: Text(_instanceHost ?? "Not configured"),
|
||||
onDetailsPressed: () {
|
||||
setState(() {
|
||||
_accountMenuExpanded = !_accountMenuExpanded;
|
||||
});
|
||||
},
|
||||
currentAccountPicture: CircleAvatar(
|
||||
child: Text(
|
||||
_homeAssistant.userAvatarText,
|
||||
style: TextStyle(
|
||||
fontSize: 32.0
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
if (_accountMenuExpanded) {
|
||||
menuItems.addAll([
|
||||
ListTile(
|
||||
leading: Icon(Icons.settings),
|
||||
title: Text("Connection settings"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/connection-settings');
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
]);
|
||||
} else {
|
||||
menuItems.addAll([
|
||||
new ListTile(
|
||||
leading: Icon(Icons.insert_drive_file),
|
||||
title: Text("Log"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/log-view');
|
||||
},
|
||||
),
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")),
|
||||
title: Text("Report an issue"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
HAUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new");
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
new AboutListTile(
|
||||
applicationName: appName,
|
||||
applicationVersion: appVersion,
|
||||
applicationLegalese: "Keyboard Crumbs | www.keyboardcrumbs.io",
|
||||
)
|
||||
]);
|
||||
}
|
||||
return new Drawer(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
new UserAccountsDrawerHeader(
|
||||
accountName: Text(_homeAssistant != null ? _homeAssistant.locationName : "Unknown"),
|
||||
accountEmail: Text(_instanceHost ?? "Not configured"),
|
||||
currentAccountPicture: new Image.asset('images/hassio-192x192.png'),
|
||||
),
|
||||
new ListTile(
|
||||
leading: Icon(Icons.settings),
|
||||
title: Text("Connection settings"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/connection-settings');
|
||||
},
|
||||
),
|
||||
new ListTile(
|
||||
leading: Icon(Icons.insert_drive_file),
|
||||
title: Text("Log"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/log-view');
|
||||
},
|
||||
),
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")),
|
||||
title: Text("Report an issue"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
HAUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new");
|
||||
},
|
||||
),
|
||||
Container(
|
||||
height: 30.0,
|
||||
decoration: new BoxDecoration(
|
||||
border: new Border(
|
||||
top: BorderSide(
|
||||
width: 2.0,
|
||||
color: Colors.black26,
|
||||
)
|
||||
),
|
||||
)
|
||||
),
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:coffee")),
|
||||
title: Text("By me a coffee"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
HAUtils.launchURL("https://www.buymeacoffee.com/estevez");
|
||||
},
|
||||
),
|
||||
new AboutListTile(
|
||||
applicationName: appName,
|
||||
applicationVersion: appVersion,
|
||||
applicationLegalese: "Keyboard Crumbs | www.keyboardcrumbs.io",
|
||||
)
|
||||
],
|
||||
children: menuItems,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_checkShowInfo(BuildContext context) {
|
||||
if (_errorCodeToBeShown > 0) {
|
||||
String message = _lastErrorMessage;
|
||||
void _hideErrorSnackBar() {
|
||||
_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
}
|
||||
|
||||
void _showErrorSnackBar({Key key, @required String message, @required int errorCode}) {
|
||||
SnackBarAction action;
|
||||
switch (_errorCodeToBeShown) {
|
||||
switch (errorCode) {
|
||||
case 9:
|
||||
case 11:
|
||||
case 7:
|
||||
@ -600,19 +424,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Timer(Duration(seconds: 1), () {
|
||||
_scaffoldKey.currentState.hideCurrentSnackBar();
|
||||
_scaffoldKey.currentState.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("$message (code: $_errorCodeToBeShown)"),
|
||||
action: action,
|
||||
duration: Duration(hours: 1),
|
||||
)
|
||||
);
|
||||
});
|
||||
} else {
|
||||
_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
}
|
||||
_scaffoldKey.currentState.hideCurrentSnackBar();
|
||||
_scaffoldKey.currentState.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("$message (code: $errorCode)"),
|
||||
action: action,
|
||||
duration: Duration(hours: 1),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||
@ -622,7 +441,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: _buildAppTitle(),
|
||||
bottom: empty ? null : TabBar(tabs: buildUIViewTabs()),
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.menu),
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.openDrawer();
|
||||
setState(() {
|
||||
_accountMenuExpanded = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
bottom: empty ? null : TabBar(
|
||||
tabs: buildUIViewTabs(),
|
||||
isScrollable: true,
|
||||
),
|
||||
),
|
||||
drawer: _buildAppDrawer(),
|
||||
body: empty ?
|
||||
@ -633,21 +464,18 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
|
||||
size: 100.0,
|
||||
color: _errorCodeToBeShown == 0 ? Colors.blue : Colors.redAccent,
|
||||
color: _isLoading == 2 ? Colors.redAccent : Colors.blue,
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
:
|
||||
TabBarView(
|
||||
children: _buildViews()
|
||||
),
|
||||
_homeAssistant.buildViews(context)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_checkShowInfo(context);
|
||||
// This method is rerun every time setState is called.
|
||||
if (_entities == null) {
|
||||
return _buildScaffold(true);
|
||||
@ -666,7 +494,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
if (_settingsSubscription != null) _settingsSubscription.cancel();
|
||||
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
|
||||
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
|
||||
_homeAssistant.closeConnection();
|
||||
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
|
||||
if (_showErrorSubscription != null) _showErrorSubscription.cancel();
|
||||
_homeAssistant.disconnect();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,12 @@ class MaterialDesignIcons {
|
||||
"input_text": "mdi:textbox",
|
||||
"sun": "mdi:white-balance-sunny",
|
||||
"scene": "mdi:google-pages",
|
||||
"media_player": "mdi:cast"
|
||||
"media_player": "mdi:cast",
|
||||
"climate": "mdi:thermostat",
|
||||
"cover.open": "mdi:window-open",
|
||||
"cover.closed": "mdi:window-closed",
|
||||
"cover.closing": "mdi:window-open",
|
||||
"cover.opening": "mdi:window-open",
|
||||
};
|
||||
|
||||
static Map _defaultIconsByDeviceClass = {
|
||||
@ -68,7 +73,14 @@ class MaterialDesignIcons {
|
||||
//"sensor.illuminance": "mdi:",
|
||||
"sensor.temperature": "mdi:thermometer",
|
||||
//"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 = {
|
||||
"mdi:access-point": 0xf002,
|
||||
@ -2915,7 +2927,7 @@ class MaterialDesignIcons {
|
||||
|
||||
static int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
||||
String domain = entityId.split(".")[0];
|
||||
String iconNameByDomain = _defaultIconsByDomains[domain];
|
||||
String iconNameByDomain = _defaultIconsByDomains["$domain.$state"] ?? _defaultIconsByDomains["$domain"];
|
||||
String iconNameByDeviceClass;
|
||||
if (deviceClass != null) {
|
||||
iconNameByDeviceClass = _defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? _defaultIconsByDeviceClass["$domain.$deviceClass"];
|
||||
|
@ -11,15 +11,29 @@ class ConnectionSettingsPage extends StatefulWidget {
|
||||
|
||||
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
String _hassioDomain = "";
|
||||
String _hassioPort = "8123";
|
||||
String _newHassioDomain = "";
|
||||
String _hassioPort = "";
|
||||
String _newHassioPort = "";
|
||||
String _hassioPassword = "";
|
||||
String _newHassioPassword = "";
|
||||
String _socketProtocol = "wss";
|
||||
String _newSocketProtocol = "wss";
|
||||
String _authType = "access_token";
|
||||
bool _connectionSettingsChanged = false;
|
||||
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();
|
||||
}
|
||||
|
||||
@ -27,26 +41,35 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
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';
|
||||
_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 (_hassioDomain.indexOf("http") == 0 && _hassioDomain.indexOf("//") > 0) {
|
||||
_hassioDomain = _hassioDomain.split("//")[1];
|
||||
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
||||
_newHassioDomain = _newHassioDomain.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);
|
||||
_connectionSettingsChanged = true;
|
||||
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
|
||||
@ -60,16 +83,12 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.check),
|
||||
onPressed:(){
|
||||
if (_connectionSettingsChanged) {
|
||||
onPressed: _edited ? (){
|
||||
_saveSettings().then((r){
|
||||
Navigator.pop(context);
|
||||
eventBus.fire(SettingsChangedEvent(_connectionSettingsChanged));
|
||||
eventBus.fire(SettingsChangedEvent(true));
|
||||
});
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
} : null
|
||||
)
|
||||
],
|
||||
),
|
||||
@ -80,12 +99,10 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
children: [
|
||||
Text("Use ssl (HTTPS)"),
|
||||
Switch(
|
||||
value: (_socketProtocol == "wss"),
|
||||
value: (_newSocketProtocol == "wss"),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_socketProtocol = value ? "wss" : "ws";
|
||||
});
|
||||
_saveSettings();
|
||||
_newSocketProtocol = value ? "wss" : "ws";
|
||||
_checkConfigChanged();
|
||||
},
|
||||
)
|
||||
],
|
||||
@ -94,36 +111,46 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
decoration: InputDecoration(
|
||||
labelText: "Home Assistant domain or ip address"
|
||||
),
|
||||
controller: TextEditingController(
|
||||
text: _hassioDomain
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _newHassioDomain,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _newHassioDomain.length)
|
||||
)
|
||||
),
|
||||
onChanged: (value) {
|
||||
_hassioDomain = value;
|
||||
_saveSettings();
|
||||
_newHassioDomain = value;
|
||||
},
|
||||
focusNode: _domainFocusNode,
|
||||
onEditingComplete: _checkConfigChanged,
|
||||
),
|
||||
new TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Home Assistant port"
|
||||
labelText: "Home Assistant port (default is 8123)"
|
||||
),
|
||||
controller: TextEditingController(
|
||||
text: _hassioPort
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _newHassioPort,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _newHassioPort.length)
|
||||
)
|
||||
),
|
||||
onChanged: (value) {
|
||||
_hassioPort = value;
|
||||
_saveSettings();
|
||||
_newHassioPort = value;
|
||||
//_saveSettings();
|
||||
},
|
||||
focusNode: _portFocusNode,
|
||||
onEditingComplete: _checkConfigChanged,
|
||||
),
|
||||
new Row(
|
||||
children: [
|
||||
Text("Login with access token (HA >= 0.78.0)"),
|
||||
Switch(
|
||||
value: (_authType == "access_token"),
|
||||
value: (_newAuthType == "access_token"),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_authType = value ? "access_token" : "api_password";
|
||||
});
|
||||
_saveSettings();
|
||||
_newAuthType = value ? "access_token" : "api_password";
|
||||
_checkConfigChanged();
|
||||
//_saveSettings();
|
||||
},
|
||||
)
|
||||
],
|
||||
@ -132,16 +159,33 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
decoration: InputDecoration(
|
||||
labelText: _authType == "access_token" ? "Access token" : "API password"
|
||||
),
|
||||
controller: TextEditingController(
|
||||
text: _hassioPassword
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _newHassioPassword,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _newHassioPassword.length)
|
||||
)
|
||||
),
|
||||
onChanged: (value) {
|
||||
_hassioPassword = value;
|
||||
_saveSettings();
|
||||
_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,66 +0,0 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class UIBuilder {
|
||||
EntityCollection _entities;
|
||||
Map<String, View> _views;
|
||||
static List badgeDomains = ["alarm_control_panel", "binary_sensor", "device_tracker", "updater", "sun", "timer", "sensor"];
|
||||
|
||||
bool get isEmpty => _views.length == 0;
|
||||
Map<String, View> get views => _views ?? {};
|
||||
|
||||
UIBuilder() {
|
||||
_views = {};
|
||||
}
|
||||
|
||||
static bool isBadge(String domain) {
|
||||
return badgeDomains.contains(domain);
|
||||
}
|
||||
|
||||
void build(EntityCollection entitiesCollection) {
|
||||
_entities = entitiesCollection;
|
||||
_views.clear();
|
||||
if (!_entities.hasDefaultView) {
|
||||
_createDefaultView();
|
||||
}
|
||||
_createViews(entitiesCollection.viewList);
|
||||
}
|
||||
|
||||
void _createDefaultView() {
|
||||
Map<String, List<String>> userGroupsList = _entities.getDefaultViewTopLevelEntities();
|
||||
TheLogger.log("RESULT", "${userGroupsList["userGroups"]}");
|
||||
TheLogger.log("RESULT", "${userGroupsList["notGroupedEntities"]}");
|
||||
View view = View("group.default_view", 0);
|
||||
userGroupsList["userGroups"].forEach((groupId){
|
||||
view.add(_entities.get(groupId));
|
||||
});
|
||||
userGroupsList["notGroupedEntities"].forEach((entityId){
|
||||
view.add(_entities.get(entityId));
|
||||
});
|
||||
_views["group.default_view"] = view;
|
||||
}
|
||||
|
||||
void _createViews(List<String> viewsList) {
|
||||
int counter = 0;
|
||||
viewsList.forEach((viewId) {
|
||||
counter += 1;
|
||||
View view = View(viewId, counter);
|
||||
|
||||
try {
|
||||
Entity viewGroupEntity = _entities.get(viewId);
|
||||
viewGroupEntity.childEntities.forEach((
|
||||
entityId) { //Each entity or group in view
|
||||
if (_entities.isExist(entityId)) {
|
||||
view.add(_entities.get(entityId));
|
||||
} else {
|
||||
TheLogger.log("Warning", "Unknown entity inside view: $entityId");
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
TheLogger.log("Error","Error parsing view: $viewId");
|
||||
}
|
||||
|
||||
_views[viewId] = view;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -56,11 +56,19 @@ class SettingsChangedEvent {
|
||||
SettingsChangedEvent(this.reconnect);
|
||||
}
|
||||
|
||||
class RefreshDataEvent {
|
||||
RefreshDataEvent();
|
||||
}
|
||||
|
||||
class RefreshDataFinishedEvent {
|
||||
RefreshDataFinishedEvent();
|
||||
}
|
||||
|
||||
class ServiceCallEvent {
|
||||
String domain;
|
||||
String service;
|
||||
String entityId;
|
||||
Map<String, String> additionalParams;
|
||||
Map<String, dynamic> additionalParams;
|
||||
|
||||
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
|
||||
}
|
||||
@ -69,4 +77,11 @@ class ShowEntityPageEvent {
|
||||
Entity entity;
|
||||
|
||||
ShowEntityPageEvent(this.entity);
|
||||
}
|
||||
|
||||
class ShowErrorEvent {
|
||||
String text;
|
||||
int errorCode;
|
||||
|
||||
ShowErrorEvent(this.text, this.errorCode);
|
||||
}
|
101
lib/view_builder.class.dart
Normal file
@ -0,0 +1,101 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class ViewBuilder{
|
||||
|
||||
EntityCollection entityCollection;
|
||||
List<View> _views;
|
||||
|
||||
ViewBuilder({
|
||||
Key key,
|
||||
this.entityCollection
|
||||
}) {
|
||||
_compose();
|
||||
}
|
||||
|
||||
Widget buildWidget(BuildContext context) {
|
||||
return ViewBuilderWidget(
|
||||
entities: _views
|
||||
);
|
||||
}
|
||||
|
||||
void _compose() {
|
||||
TheLogger.log("Debug", "Rebuilding all UI...");
|
||||
_views = [];
|
||||
if (!entityCollection.hasDefaultView) {
|
||||
_views.add(_composeDefaultView());
|
||||
}
|
||||
_views.addAll(_composeViews());
|
||||
}
|
||||
|
||||
View _composeDefaultView() {
|
||||
Map<String, List<String>> userGroupsList = entityCollection.getDefaultViewTopLevelEntities();
|
||||
List<Entity> entitiesForView = [];
|
||||
userGroupsList["userGroups"].forEach((groupId){
|
||||
entitiesForView.add(entityCollection.get(groupId));
|
||||
});
|
||||
userGroupsList["notGroupedEntities"].forEach((entityId){
|
||||
entitiesForView.add(entityCollection.get(entityId));
|
||||
});
|
||||
return View(
|
||||
entities: entitiesForView,
|
||||
count: 0
|
||||
);
|
||||
}
|
||||
|
||||
List<View> _composeViews() {
|
||||
List<View> result = [];
|
||||
int counter = 0;
|
||||
entityCollection.viewList.forEach((viewId) {
|
||||
counter += 1;
|
||||
//try {
|
||||
Entity viewGroupEntity = entityCollection.get(viewId);
|
||||
List<Entity> entitiesForView = [];
|
||||
viewGroupEntity.childEntityIds.forEach((
|
||||
entityId) { //Each entity or group in view
|
||||
if (entityCollection.isExist(entityId)) {
|
||||
Entity en = entityCollection.get(entityId);
|
||||
if (en.isGroup) {
|
||||
en.childEntities = entityCollection.getAll(en.childEntityIds);
|
||||
}
|
||||
entitiesForView.add(en);
|
||||
} else {
|
||||
TheLogger.log("Warning", "Unknown entity inside view: $entityId");
|
||||
}
|
||||
});
|
||||
result.add(View(
|
||||
count: counter,
|
||||
entities: entitiesForView
|
||||
));
|
||||
/*} catch (error) {
|
||||
TheLogger.log("Error","Error parsing view: $viewId");
|
||||
}*/
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class ViewBuilderWidget extends StatelessWidget {
|
||||
|
||||
final List<View> entities;
|
||||
|
||||
const ViewBuilderWidget({
|
||||
Key key,
|
||||
this.entities
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TabBarView(
|
||||
children: _buildChildren(context)
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildChildren(BuildContext context) {
|
||||
List<Widget> result = [];
|
||||
entities.forEach((View view){
|
||||
result.add(view.buildWidget(context));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -1,53 +1,158 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class View {
|
||||
String _entityId;
|
||||
int _count;
|
||||
Map<String, HACard> cards;
|
||||
Map<String, Badge> badges;
|
||||
List<Entity> childEntitiesAsBadges;
|
||||
Map<String, CardSkeleton> childEntitiesAsCards;
|
||||
|
||||
bool get isThereBadges => (badges != null) && (badges.isNotEmpty);
|
||||
int count;
|
||||
List<Entity> entities;
|
||||
|
||||
View(String groupId, int viewCount) {
|
||||
_entityId = groupId;
|
||||
_count = viewCount;
|
||||
cards = {};
|
||||
badges = {};
|
||||
View({
|
||||
Key key,
|
||||
this.count,
|
||||
this.entities
|
||||
}) {
|
||||
childEntitiesAsBadges = [];
|
||||
childEntitiesAsCards = {};
|
||||
_composeEntities();
|
||||
}
|
||||
|
||||
void add(Entity entity) {
|
||||
if (!entity.isGroup) {
|
||||
_addEntityWithoutGroup(entity);
|
||||
} else {
|
||||
_addCardWithEntities(entity);
|
||||
}
|
||||
Widget buildWidget(BuildContext context) {
|
||||
return ViewWidget(
|
||||
badges: childEntitiesAsBadges,
|
||||
cards: childEntitiesAsCards,
|
||||
);
|
||||
}
|
||||
|
||||
void _addBadge(String entityId) {
|
||||
badges.addAll({entityId: Badge(entityId)});
|
||||
}
|
||||
|
||||
void _addEntityWithoutGroup(Entity entity) {
|
||||
if (UIBuilder.isBadge(entity.domain)) {
|
||||
//This is badge
|
||||
_addBadge(entity.entityId);
|
||||
} else {
|
||||
//This is a standalone entity
|
||||
String groupIdToAdd = "${entity.domain}.${entity.domain}$_count";
|
||||
if (cards[groupIdToAdd] == null) {
|
||||
_addCard(groupIdToAdd, entity.domain);
|
||||
void _composeEntities() {
|
||||
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,
|
||||
);
|
||||
childEntitiesAsCards[entity.entityId].childEntities = entity.childEntities;
|
||||
}
|
||||
cards[groupIdToAdd].addEntity(entity.entityId);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ViewWidget extends StatefulWidget {
|
||||
final List<Entity> badges;
|
||||
final Map<String, CardSkeleton> cards;
|
||||
final String displayName;
|
||||
|
||||
const ViewWidget({
|
||||
Key key,
|
||||
this.badges,
|
||||
this.cards,
|
||||
this.displayName
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return ViewWidgetState();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ViewWidgetState extends State<ViewWidget> {
|
||||
|
||||
StreamSubscription _refreshDataSubscription;
|
||||
Completer _refreshCompleter;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||
if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) {
|
||||
_refreshCompleter.complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
color: Colors.amber,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: _buildChildren(context),
|
||||
),
|
||||
onRefresh: () => _refreshData(),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildChildren(BuildContext context) {
|
||||
List<Widget> result = [];
|
||||
|
||||
if (widget.badges.isNotEmpty) {
|
||||
result.insert(0,
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10.0,
|
||||
runSpacing: 1.0,
|
||||
children: _buildBadges(context, widget.badges),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
widget.cards.forEach((String id, CardSkeleton skeleton){
|
||||
result.add(
|
||||
HACard(
|
||||
entities: skeleton.childEntities,
|
||||
friendlyName: skeleton.displayName,
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void _addCard(String entityId, String friendlyName) {
|
||||
cards.addAll({"$entityId": HACard(entityId, friendlyName)});
|
||||
List<Widget> _buildBadges(BuildContext context, List<Entity> badges) {
|
||||
List<Widget> result = [];
|
||||
badges.forEach((Entity entity) {
|
||||
result.add(entity.buildBadgeWidget(context));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void _addCardWithEntities(Entity entity) {
|
||||
cards.addAll({"${entity.entityId}": HACard(entity.entityId, entity.displayName)});
|
||||
cards[entity.entityId].addEntities(entity.childEntities);
|
||||
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;
|
||||
|
||||
CardSkeleton({Key key, this.displayName, this.childEntities}) {
|
||||
childEntities = [];
|
||||
}
|
||||
}
|
13
pubspec.lock
@ -114,7 +114,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct main"
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
url: "https://pub.dartlang.org"
|
||||
@ -251,13 +251,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -327,7 +320,7 @@ packages:
|
||||
name: shared_preferences
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
version: "0.4.3"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -495,5 +488,5 @@ packages:
|
||||
source: hosted
|
||||
version: "2.1.15"
|
||||
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"
|
||||
|
@ -1,7 +1,7 @@
|
||||
name: hass_client
|
||||
description: Home Assistant Android Client
|
||||
|
||||
version: 0.2.4+26
|
||||
version: 0.3.0+38
|
||||
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
||||
@ -12,8 +12,6 @@ dependencies:
|
||||
shared_preferences: any
|
||||
progress_indicators: ^0.1.2
|
||||
event_bus: ^1.0.1
|
||||
package_info: ^0.3.2
|
||||
flutter_launcher_icons: ^0.6.1
|
||||
cached_network_image: ^0.4.1
|
||||
url_launcher: ^3.0.3
|
||||
date_format: ^1.0.5
|
||||
@ -25,6 +23,7 @@ dependencies:
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_launcher_icons: ^0.6.1
|
||||
|
||||
flutter_icons:
|
||||
android: true
|
||||
|