WIP #120 Simplest on/off state history chart
This commit is contained in:
parent
809c7d6355
commit
052cd3894e
@ -52,7 +52,10 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
|||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: EdgeInsets.all(10.0),
|
padding: EdgeInsets.all(10.0),
|
||||||
child: widget.entity.buildEntityPageWidget(context)
|
child: HomeAssistantModel(
|
||||||
|
homeAssistant: widget.homeAssistant,
|
||||||
|
child: widget.entity.buildEntityPageWidget(context)
|
||||||
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -132,14 +132,20 @@ class Entity {
|
|||||||
DefaultEntityContainer(state: _buildStatePartForPage(context), height: widgetHeight),
|
DefaultEntityContainer(state: _buildStatePartForPage(context), height: widgetHeight),
|
||||||
LastUpdatedWidget(),
|
LastUpdatedWidget(),
|
||||||
Divider(),
|
Divider(),
|
||||||
|
buildHistoryWidget(),
|
||||||
_buildAdditionalControlsForPage(context),
|
_buildAdditionalControlsForPage(context),
|
||||||
Divider(),
|
|
||||||
EntityAttributesList()
|
EntityAttributesList()
|
||||||
]),
|
]),
|
||||||
handleTap: false,
|
handleTap: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildHistoryWidget() {
|
||||||
|
return EntityHistoryWidget(
|
||||||
|
type: EntityHistoryWidgetType.simplest,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildBadgeWidget(BuildContext context) {
|
Widget buildBadgeWidget(BuildContext context) {
|
||||||
return EntityModel(
|
return EntityModel(
|
||||||
entity: this,
|
entity: this,
|
||||||
|
217
lib/entity_widgets/entity_history.dart
Normal file
217
lib/entity_widgets/entity_history.dart
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
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"]);
|
||||||
|
DateTime endTime;
|
||||||
|
if (i < (_history.length - 1)) {
|
||||||
|
endTime = DateTime.tryParse(_history[i+1]["last_updated"]);
|
||||||
|
} 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);
|
||||||
|
}
|
@ -20,3 +20,23 @@ class EntityModel extends InheritedWidget {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HomeAssistantModel extends InheritedWidget {
|
||||||
|
|
||||||
|
const HomeAssistantModel({
|
||||||
|
Key key,
|
||||||
|
@required this.homeAssistant,
|
||||||
|
@required Widget child,
|
||||||
|
}) : super(key: key, child: child);
|
||||||
|
|
||||||
|
final HomeAssistant homeAssistant;
|
||||||
|
|
||||||
|
static HomeAssistantModel of(BuildContext context) {
|
||||||
|
return context.inheritFromWidgetOfExactType(HomeAssistantModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(InheritedWidget oldWidget) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -478,7 +478,7 @@ class HomeAssistant {
|
|||||||
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
//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]);
|
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||||
TheLogger.debug( "$startTime");
|
TheLogger.debug( "$startTime");
|
||||||
String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId&skip_initial_state";
|
String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
|
||||||
TheLogger.debug( "$url");
|
TheLogger.debug( "$url");
|
||||||
http.Response historyResponse;
|
http.Response historyResponse;
|
||||||
if (_authType == "access_token") {
|
if (_authType == "access_token") {
|
||||||
@ -492,12 +492,12 @@ class HomeAssistant {
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
var _history = json.decode(historyResponse.body);
|
var history = json.decode(historyResponse.body);
|
||||||
if (_history is Map) {
|
if (history is List) {
|
||||||
return null;
|
TheLogger.debug( "${history.toString()}");
|
||||||
} else if (_history is List) {
|
return history;
|
||||||
TheLogger.debug( "${_history[0].toString()}");
|
} else {
|
||||||
return _history;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:date_format/date_format.dart';
|
import 'package:date_format/date_format.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:flutter_colorpicker/material_picker.dart';
|
import 'package:flutter_colorpicker/material_picker.dart';
|
||||||
|
import 'package:charts_flutter/flutter.dart' as charts;
|
||||||
|
|
||||||
part 'entity_class/entity.class.dart';
|
part 'entity_class/entity.class.dart';
|
||||||
part 'entity_class/switch_entity.class.dart';
|
part 'entity_class/switch_entity.class.dart';
|
||||||
@ -25,7 +26,7 @@ part 'entity_class/light_entity.class.dart';
|
|||||||
part 'entity_class/select_entity.class.dart';
|
part 'entity_class/select_entity.class.dart';
|
||||||
part 'entity_class/sun_entity.class.dart';
|
part 'entity_class/sun_entity.class.dart';
|
||||||
part 'entity_widgets/badge.dart';
|
part 'entity_widgets/badge.dart';
|
||||||
part 'entity_widgets/entity_model.dart';
|
part 'entity_widgets/model_widgets.dart';
|
||||||
part 'entity_widgets/default_entity_container.dart';
|
part 'entity_widgets/default_entity_container.dart';
|
||||||
part 'entity_widgets/entity_attributes_list.dart';
|
part 'entity_widgets/entity_attributes_list.dart';
|
||||||
part 'entity_widgets/entity_icon.dart';
|
part 'entity_widgets/entity_icon.dart';
|
||||||
@ -34,6 +35,7 @@ part 'entity_widgets/last_updated.dart';
|
|||||||
part 'entity_widgets/mode_swicth.dart';
|
part 'entity_widgets/mode_swicth.dart';
|
||||||
part 'entity_widgets/mode_selector.dart';
|
part 'entity_widgets/mode_selector.dart';
|
||||||
part 'entity_widgets/entity_page_container.dart';
|
part 'entity_widgets/entity_page_container.dart';
|
||||||
|
part 'entity_widgets/entity_history.dart';
|
||||||
part 'entity_widgets/state/switch_state.dart';
|
part 'entity_widgets/state/switch_state.dart';
|
||||||
part 'entity_widgets/state/slider_state.dart';
|
part 'entity_widgets/state/slider_state.dart';
|
||||||
part 'entity_widgets/state/text_input_state.dart';
|
part 'entity_widgets/state/text_input_state.dart';
|
||||||
@ -63,7 +65,7 @@ part 'ui_widgets/media_control_card.dart';
|
|||||||
|
|
||||||
EventBus eventBus = new EventBus();
|
EventBus eventBus = new EventBus();
|
||||||
const String appName = "HA Client";
|
const String appName = "HA Client";
|
||||||
const appVersion = "0.3.3.45";
|
const appVersion = "0.3.3.47";
|
||||||
|
|
||||||
String homeAssistantWebHost;
|
String homeAssistantWebHost;
|
||||||
|
|
||||||
|
21
pubspec.lock
21
pubspec.lock
@ -50,6 +50,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
charts_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: charts_common
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.0"
|
||||||
|
charts_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: charts_flutter
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -181,6 +195,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.4"
|
version: "2.0.4"
|
||||||
|
intl:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.7"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
name: hass_client
|
name: hass_client
|
||||||
description: Home Assistant Android Client
|
description: Home Assistant Android Client
|
||||||
|
|
||||||
version: 0.3.3+45
|
version: 0.3.3+47
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
||||||
@ -16,6 +16,7 @@ dependencies:
|
|||||||
url_launcher: ^3.0.3
|
url_launcher: ^3.0.3
|
||||||
date_format: ^1.0.5
|
date_format: ^1.0.5
|
||||||
flutter_colorpicker: ^0.1.0
|
flutter_colorpicker: ^0.1.0
|
||||||
|
charts_flutter: ^0.4.0
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
Reference in New Issue
Block a user