diff --git a/lib/entity.page.dart b/lib/entity.page.dart index a3c7563..75b5caa 100644 --- a/lib/entity.page.dart +++ b/lib/entity.page.dart @@ -52,7 +52,10 @@ class _EntityViewPageState extends State { ), body: Padding( padding: EdgeInsets.all(10.0), - child: widget.entity.buildEntityPageWidget(context) + child: HomeAssistantModel( + homeAssistant: widget.homeAssistant, + child: widget.entity.buildEntityPageWidget(context) + ) ), ); } diff --git a/lib/entity_class/entity.class.dart b/lib/entity_class/entity.class.dart index f244b2c..57b4c1d 100644 --- a/lib/entity_class/entity.class.dart +++ b/lib/entity_class/entity.class.dart @@ -132,14 +132,20 @@ class Entity { DefaultEntityContainer(state: _buildStatePartForPage(context), height: widgetHeight), LastUpdatedWidget(), Divider(), + buildHistoryWidget(), _buildAdditionalControlsForPage(context), - Divider(), EntityAttributesList() ]), handleTap: false, ); } + Widget buildHistoryWidget() { + return EntityHistoryWidget( + type: EntityHistoryWidgetType.simplest, + ); + } + Widget buildBadgeWidget(BuildContext context) { return EntityModel( entity: this, diff --git a/lib/entity_widgets/entity_history.dart b/lib/entity_widgets/entity_history.dart new file mode 100644 index 0000000..6b14618 --- /dev/null +++ b/lib/entity_widgets/entity_history.dart @@ -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 { + + 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"]); + 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( + 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_model.dart b/lib/entity_widgets/model_widgets.dart similarity index 51% rename from lib/entity_widgets/entity_model.dart rename to lib/entity_widgets/model_widgets.dart index da4119d..06fad2c 100644 --- a/lib/entity_widgets/entity_model.dart +++ b/lib/entity_widgets/model_widgets.dart @@ -15,6 +15,26 @@ class EntityModel extends InheritedWidget { return context.inheritFromWidgetOfExactType(EntityModel); } + @override + bool updateShouldNotify(InheritedWidget oldWidget) { + 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; diff --git a/lib/home_assistant.class.dart b/lib/home_assistant.class.dart index c5c0865..63f5491 100644 --- a/lib/home_assistant.class.dart +++ b/lib/home_assistant.class.dart @@ -478,7 +478,7 @@ class HomeAssistant { //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&skip_initial_state"; + String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId"; TheLogger.debug( "$url"); http.Response historyResponse; if (_authType == "access_token") { @@ -492,12 +492,12 @@ class HomeAssistant { "Content-Type": "application/json" }); } - var _history = json.decode(historyResponse.body); - if (_history is Map) { - return null; - } else if (_history is List) { - TheLogger.debug( "${_history[0].toString()}"); - return _history; + var history = json.decode(historyResponse.body); + if (history is List) { + TheLogger.debug( "${history.toString()}"); + return history; + } else { + return []; } } } diff --git a/lib/main.dart b/lib/main.dart index c8dfdbf..5069821 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,6 +13,7 @@ import 'package:flutter/services.dart'; import 'package:date_format/date_format.dart'; import 'package:http/http.dart' as http; import 'package:flutter_colorpicker/material_picker.dart'; +import 'package:charts_flutter/flutter.dart' as charts; part 'entity_class/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/sun_entity.class.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/entity_attributes_list.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_selector.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/slider_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(); const String appName = "HA Client"; -const appVersion = "0.3.3.45"; +const appVersion = "0.3.3.47"; String homeAssistantWebHost; diff --git a/pubspec.lock b/pubspec.lock index 3479da4..badec13 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -50,6 +50,20 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: transitive description: @@ -181,6 +195,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.4" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.7" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5086531..b3e8597 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: hass_client description: Home Assistant Android Client -version: 0.3.3+45 +version: 0.3.3+47 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" @@ -16,6 +16,7 @@ dependencies: url_launcher: ^3.0.3 date_format: ^1.0.5 flutter_colorpicker: ^0.1.0 + charts_flutter: ^0.4.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.