WIP #120 History widget improvements

This commit is contained in:
Yegor Vialov 2018-10-28 18:07:52 +02:00
parent 6e038b0685
commit e16338c3f2
8 changed files with 320 additions and 229 deletions

View File

@ -1,14 +1,39 @@
part of '../main.dart';
class Entity {
static const STATE_ICONS_COLORS = {
class EntityColors {
static const _stateColors = {
"on": Colors.amber,
"auto": Colors.amber,
"idle": 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
"heat": Colors.redAccent,
"cool": Colors.lightBlue,
"unavailable": Colors.black26,
"unknown": Colors.black26,
"playing": Colors.amber,
"above_horizon": Colors.amber,
"home": Colors.amber,
};
static Color stateColor(String state) {
return _stateColors[state] ?? _stateColors["default"];
}
static charts.Color historyStateColor(String state) {
Color c = stateColor(state);
return charts.Color(
r: c.red,
g: c.green,
b: c.blue,
a: c.alpha
);
}
}
class Entity {
static const badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)

View File

@ -1,217 +0,0 @@
part of '../main.dart';
class EntityHistoryWidgetType {
static const int simplest = 0;
static const int valueToTime = 0;
}
class EntityHistoryWidget extends StatefulWidget {
final int type;
const EntityHistoryWidget({Key key, @required this.type}) : super(key: key);
@override
_EntityHistoryWidgetState createState() {
return new _EntityHistoryWidgetState();
}
}
class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
List _history;
bool _needToUpdateHistory;
DateTime _selectionTimeStart;
DateTime _selectionTimeEnd;
Map<String, String> _selectionData;
int _selectedId = -1;
@override
void initState() {
super.initState();
_needToUpdateHistory = true;
}
void _loadHistory(HomeAssistant ha, String entityId) {
ha.getHistory(entityId).then((history){
setState(() {
_history = history.isNotEmpty ? history[0] : [];
_needToUpdateHistory = false;
});
}).catchError((e) {
TheLogger.error("Error loading $entityId history: $e");
setState(() {
_history = [];
_needToUpdateHistory = false;
});
});
}
@override
Widget build(BuildContext context) {
final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context);
final EntityModel entityModel = EntityModel.of(context);
final Entity entity = entityModel.entity;
if (!_needToUpdateHistory) {
_needToUpdateHistory = true;
} else {
_loadHistory(homeAssistantModel.homeAssistant, entity.entityId);
}
return _buildChart();
}
Widget _buildChart() {
List<Widget> children = [];
if (_selectionTimeStart != null) {
children.add(
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.only(right: 10.0),
child: Text(
"${_selectionData["State"]}",
style: TextStyle(
fontWeight: FontWeight.bold,
color: _selectionData["State"] == "on" ? Colors.green : Colors.red
),
),
),
Column(
children: <Widget>[
Text("${formatDate(_selectionTimeStart, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}"),
Text("${formatDate(_selectionTimeEnd ?? _selectionTimeStart, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}"),
],
)
],
)
);
} else {
children.add(
Container(height: 32.0,)
);
}
if (_history == null) {
children.add(
Text("Loading history...")
);
} else if (_history.isEmpty) {
children.add(
Text("No history for last 24h")
);
} else {
children.add(
SizedBox(
height: 70.0,
child: charts.TimeSeriesChart(
_createHistoryData(),
animate: false,
dateTimeFactory: const charts.LocalDateTimeFactory(),
primaryMeasureAxis: charts.NumericAxisSpec(
renderSpec: charts.NoneRenderSpec()
),
selectionModels: [
new charts.SelectionModelConfig(
type: charts.SelectionModelType.info,
listener: _onSelectionChanged,
)
],
behaviors: [
charts.PanAndZoomBehavior(),
],
),
)
);
}
children.add(Divider());
return Padding(
padding: EdgeInsets.fromLTRB(0.0, Entity.rowPadding, 0.0, Entity.rowPadding),
child: Column(
children: children,
),
);
}
_onSelectionChanged(charts.SelectionModel model) {
final selectedDatum = model.selectedDatum;
DateTime timeStart;
DateTime timeEnd;
int selectedId;
final measures = <String, String>{};
if ((selectedDatum.isNotEmpty) &&(selectedDatum.first.datum.endTime != null)) {
timeStart = selectedDatum.first.datum.startTime;
timeEnd = selectedDatum.first.datum.endTime;
selectedId = selectedDatum.first.datum.id;
TheLogger.debug("Selected datum length is ${selectedDatum.length}");
selectedDatum.forEach((charts.SeriesDatum datumPair) {
measures[datumPair.series.displayName] = datumPair.datum.state;
});
setState(() {
_selectionTimeStart = timeStart;
_selectionTimeEnd = timeEnd;
_selectionData = measures;
_selectedId = selectedId;
_needToUpdateHistory = false;
});
} else {
setState(() {
_needToUpdateHistory = false;
});
}
}
List<charts.Series<EntityStateHistoryMoment, DateTime>> _createHistoryData() {
List<EntityStateHistoryMoment> data = [];
DateTime now = DateTime.now();
for (var i = 0; i < _history.length; i++) {
var stateData = _history[i];
DateTime startTime = DateTime.tryParse(stateData["last_updated"])?.toLocal();
DateTime endTime;
if (i < (_history.length - 1)) {
endTime = DateTime.tryParse(_history[i+1]["last_updated"])?.toLocal();
} else {
endTime = now;
}
data.add(EntityStateHistoryMoment(stateData["state"], startTime, endTime, i));
}
data.add(EntityStateHistoryMoment(data.last.state, now, null, _history.length));
return [
new charts.Series<EntityStateHistoryMoment, DateTime>(
id: 'State',
strokeWidthPxFn: (EntityStateHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 70.0 : 40.0,
colorFn: ((EntityStateHistoryMoment historyMoment, __) {
if (historyMoment.state == "on") {
if (historyMoment.id == _selectedId) {
return charts.MaterialPalette.green.makeShades(2)[0];
} else {
return charts.MaterialPalette.green.makeShades(2)[1];
}
} else {
if (historyMoment.id == _selectedId) {
return charts.MaterialPalette.red.makeShades(2)[0];
} else {
return charts.MaterialPalette.red.makeShades(2)[1];
}
}
}),
domainFn: (EntityStateHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityStateHistoryMoment historyMoment, _) => 0,
data: data,
)
];
}
}
class EntityStateHistoryMoment {
final DateTime startTime;
final DateTime endTime;
final String state;
final int id;
EntityStateHistoryMoment(this.state, this.startTime, this.endTime, this.id);
}

View File

@ -11,8 +11,8 @@ class EntityIcon extends StatelessWidget {
child: MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entity,
Entity.iconSize,
Entity.STATE_ICONS_COLORS[entityModel.entity.state] ??
Entity.STATE_ICONS_COLORS["default"]),
EntityColors.stateColor(entityModel.entity.state)
),
),
onTap: () => entityModel.handleTap
? eventBus.fire(new ShowEntityPageEvent(entityModel.entity))

View File

@ -0,0 +1,86 @@
part of '../../main.dart';
class EntityHistoryWidgetType {
static const int simplest = 0;
static const int valueToTime = 1;
}
class EntityHistoryWidget extends StatefulWidget {
final int type;
const EntityHistoryWidget({Key key, @required this.type}) : super(key: key);
@override
_EntityHistoryWidgetState createState() {
return new _EntityHistoryWidgetState();
}
}
class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
List _history;
bool _needToUpdateHistory;
int _selectedId = -1;
@override
void initState() {
super.initState();
_needToUpdateHistory = true;
}
void _loadHistory(HomeAssistant ha, String entityId) {
ha.getHistory(entityId).then((history){
setState(() {
_history = history.isNotEmpty ? history[0] : [];
_needToUpdateHistory = false;
});
}).catchError((e) {
TheLogger.error("Error loading $entityId history: $e");
setState(() {
_history = [];
_needToUpdateHistory = false;
});
});
}
@override
Widget build(BuildContext context) {
final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context);
final EntityModel entityModel = EntityModel.of(context);
final Entity entity = entityModel.entity;
if (!_needToUpdateHistory) {
_needToUpdateHistory = true;
} else {
_loadHistory(homeAssistantModel.homeAssistant, entity.entityId);
}
return _buildChart();
}
Widget _buildChart() {
List<Widget> children = [];
if (_history == null) {
children.add(
Text("Loading history...")
);
} else if (_history.isEmpty) {
children.add(
Text("No history for last 24h")
);
} else {
children.add(
SimpleStateHistoryChartWidget(
rawHistory: _history
)
);
}
children.add(Divider());
return Padding(
padding: EdgeInsets.fromLTRB(0.0, Entity.rowPadding, 0.0, Entity.rowPadding),
child: Column(
children: children,
),
);
}
}

View File

@ -0,0 +1,197 @@
part of '../../main.dart';
class SimpleStateHistoryChartWidget extends StatefulWidget {
final rawHistory;
const SimpleStateHistoryChartWidget({Key key, this.rawHistory}) : super(key: key);
@override
State<StatefulWidget> createState() {
return new _SimpleStateHistoryChartWidgetState();
}
}
class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartWidget> {
int _selectedId = -1;
List<charts.Series<SimpleEntityStateHistoryMoment, DateTime>> _parsedHistory;
@override
Widget build(BuildContext context) {
_parsedHistory = _parseHistory();
DateTime selectedTimeStart;
DateTime selectedTimeEnd;
String selectedState;
if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) {
selectedTimeStart = _parsedHistory.first.data[_selectedId].startTime;
selectedTimeEnd = _parsedHistory.first.data[_selectedId].endTime;
selectedState = _parsedHistory.first.data[_selectedId].state;
}
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
HistoryControlWidget(
selectedTimeStart: selectedTimeStart,
selectedTimeEnd: selectedTimeEnd,
selectedState: selectedState,
onPrevTap: () => _selectPrev(),
onNextTap: () => _selectNext(),
),
SizedBox(
height: 70.0,
child: charts.TimeSeriesChart(
_parsedHistory,
animate: false,
dateTimeFactory: const charts.LocalDateTimeFactory(),
primaryMeasureAxis: charts.NumericAxisSpec(
renderSpec: charts.NoneRenderSpec()
),
selectionModels: [
new charts.SelectionModelConfig(
type: charts.SelectionModelType.info,
listener: (model) => _onSelectionChanged(model),
)
],
behaviors: [
charts.PanAndZoomBehavior(),
],
),
)
],
);
}
List<charts.Series<SimpleEntityStateHistoryMoment, DateTime>> _parseHistory() {
List<SimpleEntityStateHistoryMoment> data = [];
DateTime now = DateTime.now();
for (var i = 0; i < widget.rawHistory.length; i++) {
var stateData = widget.rawHistory[i];
DateTime startTime = DateTime.tryParse(stateData["last_updated"])?.toLocal();
DateTime endTime;
if (i < (widget.rawHistory.length - 1)) {
endTime = DateTime.tryParse(widget.rawHistory[i+1]["last_updated"])?.toLocal();
} else {
endTime = now;
}
data.add(SimpleEntityStateHistoryMoment(stateData["state"], startTime, endTime, i));
}
data.add(SimpleEntityStateHistoryMoment(data.last.state, now, null, widget.rawHistory.length));
return [
new charts.Series<SimpleEntityStateHistoryMoment, DateTime>(
id: 'State',
strokeWidthPxFn: (SimpleEntityStateHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 70.0 : 40.0,
colorFn: (SimpleEntityStateHistoryMoment historyMoment, __) => EntityColors.historyStateColor(historyMoment.state),
domainFn: (SimpleEntityStateHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (SimpleEntityStateHistoryMoment historyMoment, _) => 0,
data: data,
)
];
}
void _selectPrev() {
if (_selectedId > 0) {
setState(() {
_selectedId -= 1;
});
}
}
void _selectNext() {
if (_selectedId < (_parsedHistory.first.data.length - 2)) {
setState(() {
_selectedId += 1;
});
}
}
void _onSelectionChanged(charts.SelectionModel model) {
final selectedDatum = model.selectedDatum;
int selectedId;
if ((selectedDatum.isNotEmpty) &&(selectedDatum.first.datum.endTime != null)) {
selectedId = selectedDatum.first.datum.id;
setState(() {
_selectedId = selectedId;
});
} else {
setState(() {
});
}
}
}
class HistoryControlWidget extends StatelessWidget {
final Function onPrevTap;
final Function onNextTap;
final DateTime selectedTimeStart;
final DateTime selectedTimeEnd;
final String selectedState;
const HistoryControlWidget({Key key, this.onPrevTap, this.onNextTap, this.selectedTimeStart, this.selectedTimeEnd, this.selectedState}) : super(key: key);
@override
Widget build(BuildContext context) {
if (selectedTimeStart != null) {
return
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(Icons.chevron_left),
padding: EdgeInsets.all(0.0),
iconSize: 40.0,
onPressed: onPrevTap,
),
Expanded(
child: Padding(
padding: EdgeInsets.only(right: 10.0),
child: Text(
"$selectedState",
textAlign: TextAlign.right,
style: TextStyle(
fontWeight: FontWeight.bold,
color: EntityColors.stateColor(selectedState),
fontSize: 22.0
),
),
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("${formatDate(selectedTimeStart, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}", textAlign: TextAlign.left,),
Text("${formatDate(selectedTimeEnd ?? selectedTimeStart, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}", textAlign: TextAlign.left,),
],
),
),
IconButton(
icon: Icon(Icons.chevron_right),
padding: EdgeInsets.all(0.0),
iconSize: 40.0,
onPressed: onNextTap,
),
],
);
} else {
return Container(height: 32.0);
}
}
}
class SimpleEntityStateHistoryMoment {
final DateTime startTime;
final DateTime endTime;
final String state;
final int id;
SimpleEntityStateHistoryMoment(this.state, this.startTime, this.endTime, this.id);
}

View File

@ -477,7 +477,6 @@ class HomeAssistant {
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.debug( "$startTime");
String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
TheLogger.debug( "$url");
http.Response historyResponse;
@ -494,7 +493,7 @@ class HomeAssistant {
}
var history = json.decode(historyResponse.body);
if (history is List) {
TheLogger.debug( "${history.toString()}");
TheLogger.debug( "Got ${history.first.length} history recors");
return history;
} else {
return [];

View File

@ -35,7 +35,8 @@ part 'entity_widgets/last_updated.dart';
part 'entity_widgets/mode_swicth.dart';
part 'entity_widgets/mode_selector.dart';
part 'entity_widgets/entity_page_container.dart';
part 'entity_widgets/entity_history.dart';
part 'entity_widgets/history_chart/entity_history.dart';
part 'entity_widgets/history_chart/simple_state_history_chart.dart';
part 'entity_widgets/state/switch_state.dart';
part 'entity_widgets/state/slider_state.dart';
part 'entity_widgets/state/text_input_state.dart';
@ -65,7 +66,7 @@ part 'ui_widgets/media_control_card.dart';
EventBus eventBus = new EventBus();
const String appName = "HA Client";
const appVersion = "0.3.3.47";
const appVersion = "0.3.3.48";
String homeAssistantWebHost;

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 0.3.3+47
version: 0.3.3+48
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"