diff --git a/lib/entity_class/climate_entity.class.dart b/lib/entity_class/climate_entity.class.dart index 459b010..76af32a 100644 --- a/lib/entity_class/climate_entity.class.dart +++ b/lib/entity_class/climate_entity.class.dart @@ -4,6 +4,13 @@ class ClimateEntity extends Entity { @override double widgetHeight = 38.0; + @override + EntityHistoryConfig historyConfig = EntityHistoryConfig( + chartType: EntityHistoryWidgetType.numericAttributes, + numericState: false, + numericAttributesToShow: ["temperature", "current_temperature"] + ); + static const SUPPORT_TARGET_TEMPERATURE = 1; static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2; static const SUPPORT_TARGET_TEMPERATURE_LOW = 4; diff --git a/lib/entity_class/entity.class.dart b/lib/entity_class/entity.class.dart index d6c7efd..e1badd4 100644 --- a/lib/entity_class/entity.class.dart +++ b/lib/entity_class/entity.class.dart @@ -38,7 +38,9 @@ class Entity { List childEntities = []; List attributesToShow = ["all"]; - int historyWidgetType = EntityHistoryWidgetType.simple; + EntityHistoryConfig historyConfig = EntityHistoryConfig( + chartType: EntityHistoryWidgetType.simple + ); String get displayName => attributes["friendly_name"] ?? (attributes["name"] ?? "_"); @@ -136,7 +138,7 @@ class Entity { Widget buildHistoryWidget() { return EntityHistoryWidget( - type: historyWidgetType, + config: historyConfig, ); } diff --git a/lib/entity_class/other_entity.class.dart b/lib/entity_class/other_entity.class.dart index 4dadd4b..0cdf8ba 100644 --- a/lib/entity_class/other_entity.class.dart +++ b/lib/entity_class/other_entity.class.dart @@ -7,7 +7,10 @@ class SunEntity extends Entity { class SensorEntity extends Entity { @override - int historyWidgetType = EntityHistoryWidgetType.valueToTime; + EntityHistoryConfig historyConfig = EntityHistoryConfig( + chartType: EntityHistoryWidgetType.numericState, + numericState: true + ); SensorEntity(Map rawData) : super(rawData); diff --git a/lib/entity_widgets/entity_colors.class.dart b/lib/entity_widgets/entity_colors.class.dart index 8186905..615bca6 100644 --- a/lib/entity_widgets/entity_colors.class.dart +++ b/lib/entity_widgets/entity_colors.class.dart @@ -48,7 +48,7 @@ class EntityColors { charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(id + 1)[id].shadeDefault; return Color.fromARGB(c1.a, c1.r, c1.g, c1.b); } else { - return _stateColors["default"]; + return _stateColors["on"]; } } } diff --git a/lib/entity_widgets/history_chart/combined_history_chart.dart b/lib/entity_widgets/history_chart/combined_history_chart.dart new file mode 100644 index 0000000..2bd7544 --- /dev/null +++ b/lib/entity_widgets/history_chart/combined_history_chart.dart @@ -0,0 +1,264 @@ +part of '../../main.dart'; + +class CombinedHistoryChartWidget extends StatefulWidget { + final rawHistory; + final EntityHistoryConfig config; + + const CombinedHistoryChartWidget({Key key, @required this.rawHistory, @required this.config}) : super(key: key); + + + @override + State createState() { + return new _CombinedHistoryChartWidgetState(); + } + +} + +class _CombinedHistoryChartWidgetState extends State { + + int _selectedId = -1; + List> _parsedHistory; + + @override + Widget build(BuildContext context) { + _parsedHistory = _parseHistory(); + DateTime selectedTime; + List selectedStates = []; + List colorIndexes = []; + if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) { + selectedTime = _parsedHistory.first.data[_selectedId].time; + _parsedHistory.where((item) { return item.id == "value"; }).forEach((item) { + selectedStates.add("${item.data[_selectedId].value}"); + colorIndexes.add(item.data[_selectedId].colorId); + }); + } + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CombinedHistoryControlWidget( + selectedTimeStart: selectedTime, + selectedStates: selectedStates, + onPrevTap: () => _selectPrev(), + onNextTap: () => _selectNext(), + colorIndexes: colorIndexes, + ), + SizedBox( + height: 150.0, + child: charts.TimeSeriesChart( + _parsedHistory, + animate: false, + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: + new charts.BasicNumericTickProviderSpec(zeroBound: false)), + dateTimeFactory: const charts.LocalDateTimeFactory(), + defaultRenderer: charts.LineRendererConfig(includeArea: false), + customSeriesRenderers: [ + new charts.PointRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'valuePoints') + ], + /*primaryMeasureAxis: charts.NumericAxisSpec( + renderSpec: charts.NoneRenderSpec() + ),*/ + selectionModels: [ + new charts.SelectionModelConfig( + type: charts.SelectionModelType.info, + listener: (model) => _onSelectionChanged(model), + ) + ], + behaviors: [ + charts.PanAndZoomBehavior(), + ], + ), + ) + ], + ); + } + + double _parseToDouble(temp1) { + if (temp1 is int) { + return temp1.toDouble(); + } else if (temp1 is double) { + return temp1; + } else { + return double.tryParse("$temp1"); + } + } + + List> _parseHistory() { + TheLogger.debug(" parsing history..."); + Map> dataList = {}; + int colorIdCounter = 0; + widget.config.numericAttributesToShow.forEach((String attrName) { + TheLogger.debug(" parsing attribute $attrName"); + List data = []; + DateTime now = DateTime.now(); + for (var i = 0; i < widget.rawHistory.length; i++) { + var stateData = widget.rawHistory[i]; + DateTime time = DateTime.tryParse(stateData["last_updated"])?.toLocal(); + if (stateData["attributes"] != null) { + data.add(CombinedEntityStateHistoryMoment(_parseToDouble(stateData["attributes"]["$attrName"]), stateData["state"], time, i, colorIdCounter)); + } else { + data.add(CombinedEntityStateHistoryMoment(null, stateData["state"], time, i, colorIdCounter)); + } + } + data.add(CombinedEntityStateHistoryMoment(data.last.value, data.last.state, now, widget.rawHistory.length, colorIdCounter)); + dataList.addAll({attrName: data}); + colorIdCounter += 1; + }); + + if ((_selectedId == -1) && (dataList.isNotEmpty)) { + _selectedId = 0; + } + List> result = []; + dataList.forEach((attrName, dataItem) { + TheLogger.debug(" adding ${dataItem.length} data values"); + result.addAll([ + new charts.Series( + id: "value", + colorFn: (CombinedEntityStateHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor("_", historyMoment.colorId), + domainFn: (CombinedEntityStateHistoryMoment historyMoment, _) => historyMoment.time, + measureFn: (CombinedEntityStateHistoryMoment historyMoment, _) => historyMoment.value, + data: dataItem, + ), + new charts.Series( + id: "points", + radiusPxFn: (CombinedEntityStateHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 1.0, + colorFn: (CombinedEntityStateHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor("_", historyMoment.colorId), + domainFn: (CombinedEntityStateHistoryMoment historyMoment, _) => historyMoment.time, + measureFn: (CombinedEntityStateHistoryMoment historyMoment, _) => historyMoment.value, + data: dataItem, + )..setAttribute(charts.rendererIdKey, 'valuePoints') + ]); + }); + return result; + } + + void _selectPrev() { + if (_selectedId > 0) { + setState(() { + _selectedId -= 1; + }); + } + } + + void _selectNext() { + if (_selectedId < (_parsedHistory.first.data.length - 1)) { + setState(() { + _selectedId += 1; + }); + } + } + + void _onSelectionChanged(charts.SelectionModel model) { + final selectedDatum = model.selectedDatum; + + int selectedId; + + if (selectedDatum.isNotEmpty) { + selectedId = selectedDatum.first.datum.id; + setState(() { + _selectedId = selectedId; + }); + } else { + setState(() { + }); + } + } +} + +class CombinedHistoryControlWidget extends StatelessWidget { + + final Function onPrevTap; + final Function onNextTap; + final DateTime selectedTimeStart; + final DateTime selectedTimeEnd; + final List selectedStates; + final List colorIndexes; + + const CombinedHistoryControlWidget({Key key, this.onPrevTap, this.onNextTap, this.selectedTimeStart, this.selectedTimeEnd, this.selectedStates, @ required this.colorIndexes}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (selectedTimeStart != null) { + return + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + 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: _buildStates(), + ), + ), + _buildTime(), + IconButton( + icon: Icon(Icons.chevron_right), + padding: EdgeInsets.all(0.0), + iconSize: 40.0, + onPressed: onNextTap, + ), + ], + ); + + } else { + return Container(height: 48.0); + } + } + + Widget _buildStates() { + List children = []; + for (int i = 0; i < selectedStates.length; i++) { + children.add( + Text( + "${selectedStates[i]}", + textAlign: TextAlign.right, + style: TextStyle( + fontWeight: FontWeight.bold, + color: EntityColors.historyStateColor(selectedStates[i], colorIndexes[i]), + fontSize: 22.0 + ), + ) + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: children, + ); + } + + Widget _buildTime() { + List children = []; + children.add( + Text("${formatDate(selectedTimeStart, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}", textAlign: TextAlign.left,) + ); + if (selectedTimeEnd != null) { + children.add( + Text("${formatDate(selectedTimeEnd, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}", textAlign: TextAlign.left,) + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ); + } + +} + +class CombinedEntityStateHistoryMoment { + final DateTime time; + final double value; + final int id; + final int colorId; + final String state; + + CombinedEntityStateHistoryMoment(this.value, this.state, this.time, this.id, this.colorId); +} diff --git a/lib/entity_widgets/history_chart/entity_history.dart b/lib/entity_widgets/history_chart/entity_history.dart index face272..cba6e96 100644 --- a/lib/entity_widgets/history_chart/entity_history.dart +++ b/lib/entity_widgets/history_chart/entity_history.dart @@ -2,15 +2,24 @@ part of '../../main.dart'; class EntityHistoryWidgetType { static const int simple = 0; - static const int valueToTime = 1; - static const int randomColors = 2; + static const int numericState = 1; + static const int numericAttributes = 2; +} + +class EntityHistoryConfig { + final int chartType; + final List numericAttributesToShow; + final bool numericState; + + EntityHistoryConfig({this.chartType, this.numericAttributesToShow, this.numericState: true}); + } class EntityHistoryWidget extends StatefulWidget { - final int type; + final EntityHistoryConfig config; - const EntityHistoryWidget({Key key, @required this.type}) : super(key: key); + const EntityHistoryWidget({Key key, @required this.config}) : super(key: key); @override _EntityHistoryWidgetState createState() { @@ -74,7 +83,7 @@ class _EntityHistoryWidgetState extends State { } children.add(Divider()); return Padding( - padding: EdgeInsets.fromLTRB(0.0, Entity.rowPadding, 0.0, Entity.rowPadding), + padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, Entity.rowPadding), child: Column( children: children, ), @@ -82,20 +91,34 @@ class _EntityHistoryWidgetState extends State { } Widget _selectChartWidget() { - switch (widget.type) { + TheLogger.debug(" selecting history widget (${widget.config.chartType})"); + switch (widget.config.chartType) { + case EntityHistoryWidgetType.simple: { + TheLogger.debug(" Simple selected"); return SimpleStateHistoryChartWidget( rawHistory: _history, ); } - case EntityHistoryWidgetType.valueToTime: { + case EntityHistoryWidgetType.numericState: { + TheLogger.debug(" EntityHistory selected"); return NumericStateHistoryChartWidget( rawHistory: _history, + config: widget.config, + ); + } + + case EntityHistoryWidgetType.numericAttributes: { + TheLogger.debug(" NumericAttributes selected"); + return CombinedHistoryChartWidget( + rawHistory: _history, + config: widget.config, ); } default: { + TheLogger.debug(" Simple selected as default"); return SimpleStateHistoryChartWidget( rawHistory: _history, ); diff --git a/lib/entity_widgets/history_chart/numeric_state_history_chart.dart b/lib/entity_widgets/history_chart/numeric_state_history_chart.dart index ea630a4..d8d83bb 100644 --- a/lib/entity_widgets/history_chart/numeric_state_history_chart.dart +++ b/lib/entity_widgets/history_chart/numeric_state_history_chart.dart @@ -2,8 +2,9 @@ part of '../../main.dart'; class NumericStateHistoryChartWidget extends StatefulWidget { final rawHistory; + final EntityHistoryConfig config; - const NumericStateHistoryChartWidget({Key key, this.rawHistory}) : super(key: key); + const NumericStateHistoryChartWidget({Key key, @required this.rawHistory, @required this.config}) : super(key: key); @override @@ -86,15 +87,15 @@ class _NumericStateHistoryChartWidgetState extends State( id: 'State', - colorFn: (NumericEntityStateHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor("unavailable", historyMoment.id), + colorFn: (NumericEntityStateHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor("on", -1), domainFn: (NumericEntityStateHistoryMoment historyMoment, _) => historyMoment.time, measureFn: (NumericEntityStateHistoryMoment historyMoment, _) => historyMoment.value, data: data, ), new charts.Series( id: 'State', - radiusPxFn: (NumericEntityStateHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 2.0, - colorFn: (NumericEntityStateHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor("off", historyMoment.id), + radiusPxFn: (NumericEntityStateHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 1.0, + colorFn: (NumericEntityStateHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor("on", -1), domainFn: (NumericEntityStateHistoryMoment historyMoment, _) => historyMoment.time, measureFn: (NumericEntityStateHistoryMoment historyMoment, _) => historyMoment.value, data: data, @@ -111,7 +112,7 @@ class _NumericStateHistoryChartWidgetState extends State _selectPrev(), onNextTap: () => _selectNext(), - colorIndex: _selectedId, + colorIndex: _parsedHistory.first.data[_selectedId].colorId, ), SizedBox( height: 70.0, @@ -68,6 +68,7 @@ class _SimpleStateHistoryChartWidgetState extends State> _parseHistory() { List data = []; DateTime now = DateTime.now(); + Map cachedStates = {}; for (var i = 0; i < widget.rawHistory.length; i++) { var stateData = widget.rawHistory[i]; DateTime startTime = DateTime.tryParse(stateData["last_updated"])?.toLocal(); @@ -77,9 +78,12 @@ class _SimpleStateHistoryChartWidgetState extends State( id: 'State', strokeWidthPxFn: (SimpleEntityStateHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 70.0 : 40.0, - colorFn: (SimpleEntityStateHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor(historyMoment.state, historyMoment.id), + colorFn: (SimpleEntityStateHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor(historyMoment.state, historyMoment.colorId), domainFn: (SimpleEntityStateHistoryMoment historyMoment, _) => historyMoment.startTime, measureFn: (SimpleEntityStateHistoryMoment historyMoment, _) => 0, data: data, @@ -205,6 +209,7 @@ class SimpleEntityStateHistoryMoment { final DateTime endTime; final String state; final int id; + final int colorId; - SimpleEntityStateHistoryMoment(this.state, this.startTime, this.endTime, this.id); + SimpleEntityStateHistoryMoment(this.state, this.startTime, this.endTime, this.id, this.colorId); } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 9a8f5b2..b640678 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -41,6 +41,7 @@ part 'entity_widgets/entity_page_container.dart'; part 'entity_widgets/history_chart/entity_history.dart'; part 'entity_widgets/history_chart/simple_state_history_chart.dart'; part 'entity_widgets/history_chart/numeric_state_history_chart.dart'; +part 'entity_widgets/history_chart/combined_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';