Skip to content
This repository has been archived by the owner on Aug 9, 2023. It is now read-only.

Commit

Permalink
Feature/temperature (#110)
Browse files Browse the repository at this point in the history
* Adding PiExtras to drawer, fixes #91
  • Loading branch information
sterrenb committed Aug 21, 2020
1 parent 5083187 commit 452f2b1
Show file tree
Hide file tree
Showing 28 changed files with 793 additions and 100 deletions.
4 changes: 4 additions & 0 deletions lib/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ class KIcons {
static const IconData sleep = MaterialCommunityIcons.sleep;
static const IconData wake = MaterialCommunityIcons.bell_ring;

static const IconData temperature = MaterialCommunityIcons.fire;
static const IconData cpuLoad = MaterialCommunityIcons.server;
static const IconData memoryUsage = MaterialCommunityIcons.memory;

static const IconData themeSystem = Feather.sunrise;
static const IconData themeLight = Feather.sun;
static const IconData themeDark = Feather.moon;
Expand Down
12 changes: 12 additions & 0 deletions lib/core/convert.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ extension DateTimeWithRelative on DateTime {
String get formattedTimeShort => Jiffy(this).format('hh:mm');
}

final RegExp _regex = RegExp(r'\d+.\d+');

extension StringExtension on String {
String get capitalize {
if (this == null) {
Expand All @@ -33,4 +35,14 @@ extension StringExtension on String {

return this[0].toUpperCase() + this.substring(1);
}

List<num> getNumbers() {
if (_regex.hasMatch(this))
return _regex
.allMatches(this)
.map((RegExpMatch match) => num.tryParse(match.group(0)))
.toList();
else
return [];
}
}
6 changes: 3 additions & 3 deletions lib/core/debug/fixture_reader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const kFixturePath = 'test/fixtures/';

/// Hack fix for `flutter test` outside IDE
/// https://stackoverflow.com/questions/45780255/flutter-how-to-load-file-for-testing
String _loadFileAsString(String name, String fixturePath) {
String loadFileAsString(String name, [fixturePath = kFixturePath]) {
final String filePath = '$fixturePath$name';

String fileString;
Expand All @@ -21,8 +21,8 @@ String _loadFileAsString(String name, String fixturePath) {
/// Returns the json representation of the fixture file [name].
///
/// Typically returns a `Map<String, dynamic>` or `List<dynamic>`.
dynamic jsonFixture(String name, {fixturePath = kFixturePath}) {
final string = _loadFileAsString(name, fixturePath);
dynamic jsonFixture(String name, [fixturePath = kFixturePath]) {
final string = loadFileAsString(name, fixturePath);

try {
return jsonDecode(string);
Expand Down
14 changes: 14 additions & 0 deletions lib/dev_dependency_injection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:flutterhole/features/pihole_api/data/models/many_query_data.dart
import 'package:flutterhole/features/pihole_api/data/models/over_time_data.dart';
import 'package:flutterhole/features/pihole_api/data/models/over_time_data_clients.dart';
import 'package:flutterhole/features/pihole_api/data/models/pi_client.dart';
import 'package:flutterhole/features/pihole_api/data/models/pi_extras.dart';
import 'package:flutterhole/features/pihole_api/data/models/pi_status.dart';
import 'package:flutterhole/features/pihole_api/data/models/pi_versions.dart';
import 'package:flutterhole/features/pihole_api/data/models/query_data.dart';
Expand Down Expand Up @@ -316,6 +317,19 @@ class ApiDataSourceDev implements ApiDataSource {
PiholeSettings settings, String domain, bool isWildcard) async {
return ListResponse();
}

@override
Future<PiExtras> fetchExtras(PiholeSettings settings) async {
return PiExtras(
temperature: 34.5,
memoryUsage: 18.8,
load: [
0.12,
0.34,
0.56,
],
);
}
}

@dev
Expand Down
91 changes: 91 additions & 0 deletions lib/features/pihole_api/blocs/extras_bloc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:flutterhole/core/models/failures.dart';
import 'package:flutterhole/features/pihole_api/data/models/pi_extras.dart';
import 'package:flutterhole/features/pihole_api/data/repositories/api_repository.dart';
import 'package:flutterhole/features/settings/data/models/pihole_settings.dart';
import 'package:flutterhole/features/settings/data/repositories/settings_repository.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';

part 'extras_bloc.freezed.dart';

@freezed
abstract class ExtrasState with _$ExtrasState {
const factory ExtrasState.initial() = ExtrasStateInitial;

const factory ExtrasState.loading() = ExtrasStateLoading;

const factory ExtrasState.success(PiExtras extras) = ExtrasStateSuccess;

const factory ExtrasState.failure(Failure failure) = ExtrasStateFailure;
}

@freezed
abstract class ExtrasEvent with _$ExtrasEvent {
const factory ExtrasEvent.start() = _Start;

const factory ExtrasEvent.stop() = _Stop;

const factory ExtrasEvent.fetch() = _Fetch;
}

@singleton
class ExtrasBloc extends Bloc<ExtrasEvent, ExtrasState> {
final ApiRepository _apiRepository;
final SettingsRepository _settingsRepository;

Timer _timer;

ExtrasBloc(
this._apiRepository,
this._settingsRepository,
) : super(ExtrasState.initial());

@override
Future<void> close() async {
_timer?.cancel();
return super.close();
}

@override
Stream<ExtrasState> mapEventToState(ExtrasEvent event) async* {
yield* event.when(
start: () async* {
add(ExtrasEvent.fetch());

_timer?.cancel();
_timer = Timer.periodic(Duration(seconds: 5), (_) {
add(ExtrasEvent.fetch());
});
},
stop: () async* {
print('stopping $_timer');
_timer?.cancel();
},
fetch: () async* {
yield ExtrasState.loading();

final Either<Failure, PiholeSettings> active =
await _settingsRepository.fetchActivePiholeSettings();

yield* active.fold(
(Failure failure) async* {
yield ExtrasState.failure(failure);
},
(PiholeSettings settings) async* {
final result = await _apiRepository.fetchExtras(settings);

yield* result.fold((Failure failure) async* {
yield ExtrasState.failure(failure);
}, (extras) async* {
yield ExtrasState.success(extras);
});
},
);
},
);
}
}
3 changes: 3 additions & 0 deletions lib/features/pihole_api/data/datasources/api_data_source.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flutterhole/features/pihole_api/data/models/many_query_data.dart
import 'package:flutterhole/features/pihole_api/data/models/over_time_data.dart';
import 'package:flutterhole/features/pihole_api/data/models/over_time_data_clients.dart';
import 'package:flutterhole/features/pihole_api/data/models/pi_client.dart';
import 'package:flutterhole/features/pihole_api/data/models/pi_extras.dart';
import 'package:flutterhole/features/pihole_api/data/models/pi_versions.dart';
import 'package:flutterhole/features/pihole_api/data/models/summary.dart';
import 'package:flutterhole/features/pihole_api/data/models/toggle_status.dart';
Expand All @@ -23,6 +24,8 @@ const String kNoApiTokenNeeded = 'No password set';
abstract class ApiDataSource {
Future<SummaryModel> fetchSummary(PiholeSettings settings);

Future<PiExtras> fetchExtras(PiholeSettings settings);

Future<ToggleStatus> pingPihole(PiholeSettings settings);

Future<ToggleStatus> enablePihole(PiholeSettings settings);
Expand Down
117 changes: 91 additions & 26 deletions lib/features/pihole_api/data/datasources/api_data_source_dio.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutterhole/core/convert.dart';

import 'package:alice/alice.dart';
import 'package:dio/adapter.dart';
Expand All @@ -15,14 +16,18 @@ import 'package:flutterhole/features/pihole_api/data/models/many_query_data.dart
import 'package:flutterhole/features/pihole_api/data/models/over_time_data.dart';
import 'package:flutterhole/features/pihole_api/data/models/over_time_data_clients.dart';
import 'package:flutterhole/features/pihole_api/data/models/pi_client.dart';
import 'package:flutterhole/features/pihole_api/data/models/pi_extras.dart';
import 'package:flutterhole/features/pihole_api/data/models/pi_versions.dart';
import 'package:flutterhole/features/pihole_api/data/models/summary.dart';
import 'package:flutterhole/features/pihole_api/data/models/toggle_status.dart';
import 'package:flutterhole/features/pihole_api/data/models/top_items.dart';
import 'package:flutterhole/features/pihole_api/data/models/top_sources.dart';
import 'package:flutterhole/features/pihole_api/data/models/whitelist.dart';
import 'package:flutterhole/features/settings/data/models/pihole_settings.dart';
import 'package:html/dom.dart';
import 'package:html/parser.dart';
import 'package:injectable/injectable.dart';
import 'package:supercharged/supercharged.dart';

@prod
@Injectable(as: ApiDataSource)
Expand All @@ -36,10 +41,7 @@ class ApiDataSourceDio implements ApiDataSource {
final Dio _dio;
final Alice _alice;

Future<dynamic> _get(
PiholeSettings settings, {
Map<String, dynamic> queryParameters = const {},
}) async {
void _checkAllowSelfSignedCertificates(PiholeSettings settings) {
if (settings.allowSelfSignedCertificates) {
// https://github.com/flutterchina/dio/issues/32#issuecomment-487401443
(_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
Expand All @@ -49,12 +51,10 @@ class ApiDataSourceDio implements ApiDataSource {
return client;
};
}
}

Map<String, String> headers = {
HttpHeaders.userAgentHeader: "flutterhole",
HttpHeaders.contentTypeHeader: Headers.jsonContentType,
};

void _addBasicAuthHeaders(
PiholeSettings settings, Map<String, String> headers) {
if (settings.basicAuthenticationUsername.isNotEmpty ||
settings.basicAuthenticationPassword.isNotEmpty) {
String basicAuth = 'Basic ' +
Expand All @@ -63,6 +63,39 @@ class ApiDataSourceDio implements ApiDataSource {

headers.putIfAbsent(HttpHeaders.authorizationHeader, () => basicAuth);
}
}

void _parseDioError(DioError e) {
switch (e.type) {
case DioErrorType.CONNECT_TIMEOUT:
case DioErrorType.SEND_TIMEOUT:
case DioErrorType.RECEIVE_TIMEOUT:
throw TimeOutPiException(e);
case DioErrorType.RESPONSE:
throw NotFoundPiException(e);
case DioErrorType.CANCEL:
case DioErrorType.DEFAULT:
default:
switch (e.response?.statusCode ?? 0) {
case 404:
throw NotFoundPiException(e);
default:
throw MalformedResponsePiException(e);
}
}
}

Future<dynamic> _get(
PiholeSettings settings, {
Map<String, dynamic> queryParameters = const {},
}) async {
Map<String, String> headers = {
HttpHeaders.userAgentHeader: "flutterhole",
HttpHeaders.contentTypeHeader: Headers.jsonContentType,
};

_checkAllowSelfSignedCertificates(settings);
_addBasicAuthHeaders(settings, headers);

try {
final url = '${settings.baseUrl}:${settings.apiPort}${settings.apiPath}';
Expand All @@ -89,23 +122,7 @@ class ApiDataSourceDio implements ApiDataSource {

return data;
} on DioError catch (e) {
switch (e.type) {
case DioErrorType.CONNECT_TIMEOUT:
case DioErrorType.SEND_TIMEOUT:
case DioErrorType.RECEIVE_TIMEOUT:
throw TimeOutPiException(e);
case DioErrorType.RESPONSE:
throw NotFoundPiException(e);
case DioErrorType.CANCEL:
case DioErrorType.DEFAULT:
default:
switch (e.response?.statusCode ?? 0) {
case 404:
throw NotFoundPiException(e);
default:
throw MalformedResponsePiException(e);
}
}
_parseDioError(e);
}
}

Expand Down Expand Up @@ -141,6 +158,54 @@ class ApiDataSourceDio implements ApiDataSource {
return SummaryModel.fromJson(json);
}

@override
Future<PiExtras> fetchExtras(PiholeSettings settings) async {
Map<String, String> headers = {
HttpHeaders.userAgentHeader: "flutterhole",
};

_checkAllowSelfSignedCertificates(settings);
_addBasicAuthHeaders(settings, headers);

try {
final url = '${settings.baseUrl}:${settings.apiPort}/admin/';

final Response response = await _dio.get(
url,
options: Options(
headers: headers,
responseType: ResponseType.plain,
),
);

if (response.data is String) {
if (response.data.isEmpty)
throw EmptyResponsePiException(response?.toString() ?? '');
}

final Document document = parse(response.data);

PiExtras extras = PiExtras(
temperature:
double.tryParse(document.getElementById('rawtemp').innerHtml));

final List<Element> info =
document.getElementsByClassName('fa fa-circle text-green-light');

if (info.length >= 3) {
extras = extras.copyWith(
load: info.elementAt(1).parent.innerHtml.getNumbers(),
memoryUsage:
info.elementAt(2).parent.innerHtml.getNumbers().firstOrNull(),
);
}

return extras;
} on DioError catch (e) {
_parseDioError(e);
}
}

@override
Future<ToggleStatus> pingPihole(PiholeSettings settings) async {
final Map<String, dynamic> json = await _get(settings, queryParameters: {
Expand Down
2 changes: 1 addition & 1 deletion lib/features/pihole_api/data/models/pi_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ abstract class PiClient extends MapModel implements _$PiClient {
_$PiClientFromJson(json);

String get nameOrIp => (name?.isEmpty ?? true) ? ip : '${ip} (${name})';
}
}
17 changes: 17 additions & 0 deletions lib/features/pihole_api/data/models/pi_extras.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'pi_extras.freezed.dart';
part 'pi_extras.g.dart';

@freezed
abstract class PiExtras with _$PiExtras {
const factory PiExtras({
num temperature,
List<num> load,
num memoryUsage,
}) = _PiExtras;

factory PiExtras.fromJson(Map<String, dynamic> json) =>
_$PiExtrasFromJson(json);
}

0 comments on commit 452f2b1

Please sign in to comment.