From e16338c3f23a8795d50e623a1d75956616301c24 Mon Sep 17 00:00:00 2001 From: Yegor Vialov Date: Sun, 28 Oct 2018 18:07:52 +0200 Subject: [PATCH] WIP #120 History widget improvements --- lib/entity_class/entity.class.dart | 35 ++- lib/entity_widgets/entity_history.dart | 217 ------------------ lib/entity_widgets/entity_icon.dart | 4 +- .../history_chart/entity_history.dart | 86 +++++++ .../simple_state_history_chart.dart | 197 ++++++++++++++++ lib/home_assistant.class.dart | 3 +- lib/main.dart | 5 +- pubspec.yaml | 2 +- 8 files changed, 320 insertions(+), 229 deletions(-) delete mode 100644 lib/entity_widgets/entity_history.dart create mode 100644 lib/entity_widgets/history_chart/entity_history.dart create mode 100644 lib/entity_widgets/history_chart/simple_state_history_chart.dart diff --git a/lib/entity_class/entity.class.dart b/lib/entity_class/entity.class.dart index 57b4c1d..dd5d198 100644 --- a/lib/entity_class/entity.class.dart +++ b/lib/entity_class/entity.class.dart @@ -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) diff --git a/lib/entity_widgets/entity_history.dart b/lib/entity_widgets/entity_history.dart deleted file mode 100644 index 71555d8..0000000 --- a/lib/entity_widgets/entity_history.dart +++ /dev/null @@ -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 { - - List _history; - bool _needToUpdateHistory; - DateTime _selectionTimeStart; - DateTime _selectionTimeEnd; - Map _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 children = []; - if (_selectionTimeStart != null) { - children.add( - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 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: [ - 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 = {}; - - 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> _createHistoryData() { - List 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( - 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); -} \ No newline at end of file diff --git a/lib/entity_widgets/entity_icon.dart b/lib/entity_widgets/entity_icon.dart index 761f632..46d85f5 100644 --- a/lib/entity_widgets/entity_icon.dart +++ b/lib/entity_widgets/entity_icon.dart @@ -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)) diff --git a/lib/entity_widgets/history_chart/entity_history.dart b/lib/entity_widgets/history_chart/entity_history.dart new file mode 100644 index 0000000..34a48bd --- /dev/null +++ b/lib/entity_widgets/history_chart/entity_history.dart @@ -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 { + + 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 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, + ), + ); + } + +} \ No newline at end of file diff --git a/lib/entity_widgets/history_chart/simple_state_history_chart.dart b/lib/entity_widgets/history_chart/simple_state_history_chart.dart new file mode 100644 index 0000000..7300f03 --- /dev/null +++ b/lib/entity_widgets/history_chart/simple_state_history_chart.dart @@ -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 createState() { + return new _SimpleStateHistoryChartWidgetState(); + } + +} + +class _SimpleStateHistoryChartWidgetState extends State { + + int _selectedId = -1; + List> _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: [ + 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> _parseHistory() { + List 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( + 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: [ + 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: [ + 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); +} \ No newline at end of file diff --git a/lib/home_assistant.class.dart b/lib/home_assistant.class.dart index 63f5491..c8f4cbc 100644 --- a/lib/home_assistant.class.dart +++ b/lib/home_assistant.class.dart @@ -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 []; diff --git a/lib/main.dart b/lib/main.dart index 5069821..06665dd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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; diff --git a/pubspec.yaml b/pubspec.yaml index b3e8597..b06527f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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"