summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorScott Murray <scott.murray@konsulko.com>2022-12-28 15:05:26 -0500
committerScott Murray <scott.murray@konsulko.com>2022-12-29 06:35:38 +0000
commit4fbd3fdb9e01c197d972b78961f0d033534a5cc7 (patch)
treedef7bfc0d0f11746006439b33019b61dfc16e1b8 /lib
parente21709c9601209e26d09dea0a45e37f0636bb605 (diff)
Add volume control to bottom panel
Changes: - Import a reworked version of the KUKSA.val client code from the Flutter dashboard app, with the aggregated signal Riverpod provider replaced with per-signal providers for the signal the homescreen needs and a couple of temperature ones it might use. Using separate providers is more in-line with recommended Riverpod best practices. - Various tweaks to enable using Riverpod. - Split the bottom panel out into its own widget, and add a stack in it to layer the default logo panel with the volume control slider, which has been added as a new widget definition to provide the hook to drive timer based lowering behavior like the Qt homescreen does. - The KUKSA.val connection widget has been added to the bottom panel rather than overriding the top-level widget as in the dashboard and HVAC apps. This seems preferable with respect to still providing some functionality in the event KUKSA.val is unavailable. - Remove the old demo dashboard and HVAC pages that are now unused, along with the image assets they needed, to allow cleaning up pubspec.yaml and ease maintenance. Bug-AGL: SPEC-4659 Signed-off-by: Scott Murray <scott.murray@konsulko.com> Change-Id: I5d9180a3461948a58321564e71134c4961ce0ef7
Diffstat (limited to 'lib')
-rw-r--r--lib/bottom_panel.dart84
-rw-r--r--lib/config.dart115
-rw-r--r--lib/homescreen.dart125
-rw-r--r--lib/homescreen_model.dart68
-rw-r--r--lib/main.dart18
-rw-r--r--lib/page_apps.dart43
-rw-r--r--lib/page_dashboard.dart218
-rw-r--r--lib/page_hvac.dart313
-rw-r--r--lib/vehicle-signals/viss_config.dart29
-rw-r--r--lib/vehicle-signals/viss_connected_widget.dart65
-rw-r--r--lib/vehicle-signals/viss_connection_widget.dart41
-rw-r--r--lib/vehicle-signals/viss_methods.dart116
-rw-r--r--lib/vehicle-signals/vss_path.dart44
-rw-r--r--lib/vehicle-signals/vss_providers.dart130
-rw-r--r--lib/volume_slider.dart50
15 files changed, 766 insertions, 693 deletions
diff --git a/lib/bottom_panel.dart b/lib/bottom_panel.dart
new file mode 100644
index 0000000..f61c59d
--- /dev/null
+++ b/lib/bottom_panel.dart
@@ -0,0 +1,84 @@
+import 'package:async/async.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:jovial_svg/jovial_svg.dart';
+import 'package:flutter_homescreen/volume_slider.dart';
+
+final StackIndexProvider = StateProvider<int>((ref) => 0);
+
+class BottomPanelWidget extends ConsumerStatefulWidget {
+ final double height;
+ final Color? color;
+
+ BottomPanelWidget({Key? key, required this.height, required this.color})
+ : super(key: key);
+
+ @override
+ _BottomPanelWidgetState createState() => _BottomPanelWidgetState();
+}
+
+class _BottomPanelWidgetState extends ConsumerState<BottomPanelWidget> {
+ final iconColor = const Color(0xff4ee6f5);
+ late RestartableTimer timer;
+
+ initState() {
+ super.initState();
+ timer = new RestartableTimer(Duration(seconds: 3), _timerExpired);
+ }
+
+ void _timerExpired() {
+ ref.read(StackIndexProvider.notifier).state = 0;
+ timer.cancel();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final int index = ref.watch(StackIndexProvider);
+
+ return SizedBox(
+ height: widget.height,
+ child: IndexedStack(index: index, children: <Widget>[
+ GestureDetector(
+ child: Container(
+ color: widget.color,
+ child: Align(
+ alignment: Alignment.center,
+ child: ScalableImageWidget.fromSISource(
+ si: ScalableImageSource.fromSvg(
+ rootBundle, 'images/Utility_Logo_Grey-01.svg')))),
+ onTap: () {
+ ref.read(StackIndexProvider.notifier).state = 1;
+ timer.reset();
+ },
+ ),
+ Container(
+ color: widget.color,
+ child: Padding(
+ padding: EdgeInsets.fromLTRB(24, 8, 24, 8),
+ child: Stack(children: [
+ Column(children: [
+ Center(
+ child: Text("Volume",
+ style: TextStyle(
+ fontSize: 32, color: Colors.white))),
+ Spacer()
+ ]),
+ Row(children: [
+ Container(width: 24),
+ Text("0 %",
+ style: TextStyle(fontSize: 32, color: Colors.white)),
+ Expanded(
+ child: VolumeSlider(
+ thumbColor: Colors.white,
+ activeColor: iconColor,
+ inactiveColor: Colors.grey.shade600,
+ activityTimer: timer)),
+ Text("100 %",
+ style: TextStyle(fontSize: 32, color: Colors.white)),
+ Container(width: 24)
+ ]),
+ ])))
+ ]));
+ }
+}
diff --git a/lib/config.dart b/lib/config.dart
new file mode 100644
index 0000000..6da02ed
--- /dev/null
+++ b/lib/config.dart
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: Apache-2.0
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:yaml/yaml.dart';
+
+import 'vehicle-signals/viss_connection_widget.dart';
+
+class GetConfig extends ConsumerStatefulWidget {
+ const GetConfig({Key? key, required this.client}) : super(key: key);
+ final HttpClient client;
+
+ @override
+ ConsumerState<GetConfig> createState() => _GetConfigState();
+}
+
+class _GetConfigState extends ConsumerState<GetConfig> {
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+ final configStateProvider = ref.read(ConfigStateprovider.notifier);
+
+ String configFilePath = '/etc/xdg/AGL/homescreen_config.yaml';
+
+ final configFile = File(configFilePath);
+ configFile.readAsString().then((content) {
+ final dynamic yamlMap = loadYaml(content);
+ configStateProvider.update(
+ read: true,
+ hostname: yamlMap['hostname'],
+ port: yamlMap['port'],
+ kuksaAuthToken: yamlMap['kuskaAuthToken'],
+ );
+ }).catchError((content) {
+ configStateProvider.update(read: true);
+ });
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final config = ref.watch(ConfigStateprovider);
+ if (!config.read) {
+ return Container(
+ child: const Text("Reading configuration file!",
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ color: Colors.white)));
+ } else if (config.hostname == "" ||
+ config.port == 0 ||
+ config.kuksaAuthToken == "") {
+ return Container(
+ child: const Text("Invalid configuration file!",
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ color: Colors.white)));
+ }
+ return VISServerConnectionWidget(client: widget.client);
+ }
+}
+
+class Config {
+ Config({
+ required this.read,
+ required this.hostname,
+ required this.port,
+ required this.kuksaAuthToken,
+ });
+ final bool read;
+ final String hostname;
+ final int port;
+ final String kuksaAuthToken;
+
+ Config copywith({
+ bool? read,
+ String? hostname,
+ int? port,
+ String? kuksaAuthToken,
+ }) =>
+ Config(
+ read: read ?? this.read,
+ hostname: hostname ?? this.hostname,
+ port: port ?? this.port,
+ kuksaAuthToken: kuksaAuthToken ?? this.kuksaAuthToken,
+ );
+}
+
+final ConfigStateprovider = StateNotifierProvider<ConfigStateNotifier, Config>(
+ (ref) => ConfigStateNotifier());
+
+class ConfigStateNotifier extends StateNotifier<Config> {
+ ConfigStateNotifier() : super(_initialValue);
+ static final Config _initialValue = Config(
+ read: false,
+ hostname: "",
+ port: 0,
+ kuksaAuthToken: "",
+ );
+ void update({
+ bool? read,
+ String? hostname,
+ int? port,
+ String? kuksaAuthToken,
+ }) {
+ state = state.copywith(
+ read: read,
+ hostname: hostname,
+ port: port,
+ kuksaAuthToken: kuksaAuthToken,
+ );
+ }
+}
diff --git a/lib/homescreen.dart b/lib/homescreen.dart
index d666bd3..7501292 100644
--- a/lib/homescreen.dart
+++ b/lib/homescreen.dart
@@ -1,17 +1,18 @@
+import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:provider/provider.dart';
+import 'package:flutter_homescreen/config.dart';
import 'package:grpc/grpc.dart';
-import 'package:jovial_svg/jovial_svg.dart';
import 'package:flutter_homescreen/generated/applauncher.pbgrpc.dart';
-import 'package:flutter_homescreen/homescreen_model.dart';
import 'package:flutter_homescreen/page_apps.dart';
import 'package:flutter_homescreen/widget_clock.dart';
+import 'package:flutter_homescreen/bottom_panel.dart';
enum PageIndex { home, dashboard, hvac, media }
class Homescreen extends StatefulWidget {
- Homescreen({Key? key}) : super(key: key);
+ Homescreen({Key? key, required this.client}) : super(key: key);
+ final HttpClient client;
@override
_HomescreenState createState() => _HomescreenState();
@@ -49,7 +50,8 @@ class _HomescreenState extends State<Homescreen> with TickerProviderStateMixin {
activateApp(String id) async {
try {
- agl_shell_channel.invokeMethod('activate_app', { 'app_id': id, 'index': 0 });
+ agl_shell_channel
+ .invokeMethod('activate_app', {'app_id': id, 'index': 0});
} catch (e) {
print('Could not invoke flutter/agl_shell/activate_app: $e');
}
@@ -82,7 +84,7 @@ class _HomescreenState extends State<Homescreen> with TickerProviderStateMixin {
}
}
}
- } catch(e) {
+ } catch (e) {
print(e);
}
}
@@ -149,9 +151,9 @@ class _HomescreenState extends State<Homescreen> with TickerProviderStateMixin {
var railSize = 160.0;
var iconSize = railSize / 2;
var foregroundColor = Theme.of(context)
- .navigationBarTheme
- .iconTheme!
- .resolve({MaterialState.pressed})!.color!;
+ .navigationBarTheme
+ .iconTheme!
+ .resolve({MaterialState.pressed})!.color!;
return Scaffold(
body: Column(
@@ -211,9 +213,8 @@ class _HomescreenState extends State<Homescreen> with TickerProviderStateMixin {
endIndent: railSize / 16)),
Container(
color: NavigationBarTheme.of(context).backgroundColor,
- child: ClockWidget(
- textColor: foregroundColor,
- size: railSize)),
+ child:
+ ClockWidget(textColor: foregroundColor, size: railSize)),
Container(
color: NavigationBarTheme.of(context).backgroundColor,
child: VerticalDivider(
@@ -225,70 +226,64 @@ class _HomescreenState extends State<Homescreen> with TickerProviderStateMixin {
Container(
color: NavigationBarTheme.of(context).backgroundColor,
child: Column(
- crossAxisAlignment: CrossAxisAlignment.center,
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- children: <Widget>[
- Icon(Icons.bluetooth, color: foregroundColor, size: 32),
- Icon(Icons.wifi, color: foregroundColor, size: 32),
- Icon(Icons.signal_cellular_4_bar, color: foregroundColor, size: 32),
- ])),
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: <Widget>[
+ Icon(Icons.bluetooth, color: foregroundColor, size: 32),
+ Icon(Icons.wifi, color: foregroundColor, size: 32),
+ Icon(Icons.signal_cellular_4_bar,
+ color: foregroundColor, size: 32),
+ ])),
SizedBox(
width: 16,
child: Container(
- color:
- Theme.of(context).navigationBarTheme.backgroundColor)),
+ color: Theme.of(context)
+ .navigationBarTheme
+ .backgroundColor)),
]),
),
// This is the main content.
Expanded(
- child: ChangeNotifierProvider(
- // See: https://docs.flutter.dev/development/data-and-backend/state-mgmt/simple
- // Also: https://docs.flutter.dev/development/data-and-backend/state-mgmt/options
- create: (context) => HomescreenModel(),
- child: AnimatedSwitcher(
- duration: const Duration(milliseconds: 500),
- reverseDuration: const Duration(milliseconds: 500),
- switchInCurve: Curves.easeInOut,
- switchOutCurve: Curves.easeInOut,
- transitionBuilder: (Widget child, Animation<double> animation) {
- if (child.key != ValueKey(_selectedIndex)) {
- return FadeTransition(
- opacity: Tween<double>(begin: 1.0, end: 1.0)
- .animate(animation),
- child: child,
- );
- }
- Offset beginOffset = new Offset(
- 0.0, (_selectedIndex > _previousIndex ? 1.0 : -1.0));
- return SlideTransition(
- position:
- Tween<Offset>(begin: beginOffset, end: Offset.zero)
- .animate(animation),
- child: FadeTransition(
- opacity: Tween<double>(begin: 0.0, end: 1.0).animate(
- CurvedAnimation(
- parent: animation,
- curve: Interval(0.5, 1.0),
- ),
+ child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 500),
+ reverseDuration: const Duration(milliseconds: 500),
+ switchInCurve: Curves.easeInOut,
+ switchOutCurve: Curves.easeInOut,
+ transitionBuilder: (Widget child, Animation<double> animation) {
+ if (child.key != ValueKey(_selectedIndex)) {
+ return FadeTransition(
+ opacity:
+ Tween<double>(begin: 1.0, end: 1.0).animate(animation),
+ child: child,
+ );
+ }
+ Offset beginOffset = new Offset(
+ 0.0, (_selectedIndex > _previousIndex ? 1.0 : -1.0));
+ return SlideTransition(
+ position: Tween<Offset>(begin: beginOffset, end: Offset.zero)
+ .animate(animation),
+ child: FadeTransition(
+ opacity: Tween<double>(begin: 0.0, end: 1.0).animate(
+ CurvedAnimation(
+ parent: animation,
+ curve: Interval(0.5, 1.0),
),
- child: child,
),
- );
- },
- child: _childForIndex(_selectedIndex),
- ),
+ child: child,
+ ),
+ );
+ },
+ child: _childForIndex(_selectedIndex),
),
),
- SizedBox(
- height: railSize,
- child: Container(
- color: NavigationBarTheme.of(context).backgroundColor,
- child: Align(
- alignment: Alignment.center,
- child: ScalableImageWidget.fromSISource(
- si: ScalableImageSource.fromSvg(rootBundle,
- 'images/Utility_Logo_Grey-01.svg'))))
- )
+ Stack(children: [
+ BottomPanelWidget(
+ height: railSize,
+ color: NavigationBarTheme.of(context).backgroundColor),
+ Align(
+ alignment: Alignment.bottomLeft,
+ child: GetConfig(client: widget.client))
+ ]),
],
),
);
diff --git a/lib/homescreen_model.dart b/lib/homescreen_model.dart
deleted file mode 100644
index 7c1a26f..0000000
--- a/lib/homescreen_model.dart
+++ /dev/null
@@ -1,68 +0,0 @@
-import 'dart:collection';
-
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-
-enum SwitchId {
- hvacLeftSeat,
- hvacRigthSeat,
- hvacAc,
- hvacAuto,
- hvacCirculation,
- hvacFan,
- hvacAirDown,
- hvacAirUp,
- hvacFront,
- hvacRear,
-}
-
-enum TemperatureId { leftSeat, rightSeat }
-
-class HomescreenModel extends ChangeNotifier {
- // HVAC page
-
- // fan speed
- double _fanSpeed = 20;
-
- double get fanSpeed => _fanSpeed;
-
- set fanSpeed(double newhvacFanSpeed) {
- _fanSpeed = newhvacFanSpeed;
- notifyListeners();
- }
-
- // switch buttons
- HashMap _switches = new HashMap<SwitchId, bool>();
-
- bool getSwitchState(SwitchId id) => _switches[id] ?? false;
-
- void setSwitchState(SwitchId id, bool newValue) {
- _switches[id] = newValue;
- notifyListeners();
- }
-
- void flipSwitch(SwitchId id) {
- _switches[id] = !_switches[id];
- notifyListeners();
- }
-
- // temperatures
- HashMap _temperatures = new HashMap<TemperatureId, int>();
-
- int getTemperature(TemperatureId id) => _temperatures[id] ?? 22;
-
- void setTemperature(TemperatureId id, int newTemp) {
- _temperatures[id] = newTemp;
- notifyListeners();
- }
-
- HomescreenModel() {
- // initialize the values
- for (var id in SwitchId.values) {
- _switches[id] = false;
- }
- for (var id in TemperatureId.values) {
- _temperatures[id] = 22;
- }
- }
-}
diff --git a/lib/main.dart b/lib/main.dart
index c09f328..f5ea0f7 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,14 +1,22 @@
+import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_homescreen/homescreen.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_homescreen/vehicle-signals/viss_config.dart';
-void main() {
- runApp(MyApp());
+Future<void> main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+ HttpClient client = await initializeClient();
+ runApp(ProviderScope(child: MyApp(client: client)));
}
-class MyApp extends StatelessWidget {
+class MyApp extends ConsumerWidget {
+ MyApp({Key? key, required this.client}) : super(key: key);
+ final HttpClient client;
+
// This widget is the root of your application.
@override
- Widget build(BuildContext context) {
+ Widget build(BuildContext context, WidgetRef ref) {
const navBarColor = const Color(0xff181818);
const navBarIconColor = Colors.white;
@@ -24,7 +32,7 @@ class MyApp extends StatelessWidget {
color: navBarIconColor,
))),
),
- home: Homescreen(),
+ home: Homescreen(client: client),
);
}
}
diff --git a/lib/page_apps.dart b/lib/page_apps.dart
index 39fb754..9ea6c92 100644
--- a/lib/page_apps.dart
+++ b/lib/page_apps.dart
@@ -1,9 +1,7 @@
import 'dart:io';
import 'dart:convert';
import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
import 'package:jovial_svg/jovial_svg.dart';
-import 'package:flutter_homescreen/homescreen.dart';
import 'package:flutter_homescreen/layout_size_helper.dart';
import 'package:flutter_homescreen/generated/applauncher.pb.dart';
@@ -11,7 +9,7 @@ import 'package:flutter_homescreen/generated/applauncher.pb.dart';
class AppsPage extends StatefulWidget {
final Future<List<AppInfo>> Function() getApps;
final Function(String id) startApp;
-
+
const AppsPage({Key? key, required this.getApps, required this.startApp})
: super(key: key);
@@ -26,7 +24,7 @@ class _AppsPageState extends State<AppsPage> {
initState() {
widget.getApps().then((val) => setState(() {
apps = val;
- }));
+ }));
super.initState();
}
@@ -94,8 +92,8 @@ class _AppsPageEntryState extends State<_AppsPageEntry> {
void initState() {
if (widget.iconPath.endsWith(".svg")) {
readSvgIcon().then((val) => setState(() {
- svgIconLoaded = val;
- }));
+ svgIconLoaded = val;
+ }));
}
super.initState();
}
@@ -104,7 +102,8 @@ class _AppsPageEntryState extends State<_AppsPageEntry> {
if (widget.iconPath.endsWith(".svg")) {
var iconFile = File(widget.iconPath);
if (await iconFile.exists()) {
- svgIcon = await ScalableImage.fromSvgStream(iconFile.openRead().transform(utf8.decoder));
+ svgIcon = await ScalableImage.fromSvgStream(
+ iconFile.openRead().transform(utf8.decoder));
return true;
}
}
@@ -114,25 +113,21 @@ class _AppsPageEntryState extends State<_AppsPageEntry> {
Widget buildIcon() {
if (svgIconLoaded) {
return GestureDetector(
- onTap: () {
- widget.appSelected(widget.id);
- },
- child: SizedBox.expand(
- child: ScalableImageWidget(si: svgIcon))
- );
+ onTap: () {
+ widget.appSelected(widget.id);
+ },
+ child: SizedBox.expand(child: ScalableImageWidget(si: svgIcon)));
} else {
return OutlinedButton(
- style: ElevatedButton.styleFrom(
- shape: CircleBorder(),
- padding: EdgeInsets.all(8),
- side: BorderSide(width: 4, color: iconColor),
- ),
- onPressed: () {
- widget.appSelected(widget.id);
- },
- child: Icon(Icons.question_mark,
- color: iconColor,
- size: 160.0));
+ style: ElevatedButton.styleFrom(
+ shape: CircleBorder(),
+ padding: EdgeInsets.all(8),
+ side: BorderSide(width: 4, color: iconColor),
+ ),
+ onPressed: () {
+ widget.appSelected(widget.id);
+ },
+ child: Icon(Icons.question_mark, color: iconColor, size: 160.0));
}
}
diff --git a/lib/page_dashboard.dart b/lib/page_dashboard.dart
deleted file mode 100644
index c354415..0000000
--- a/lib/page_dashboard.dart
+++ /dev/null
@@ -1,218 +0,0 @@
-import 'dart:async';
-import 'dart:math';
-
-import 'package:flutter/material.dart';
-import 'package:flutter_homescreen/layout_size_helper.dart';
-
-// The Dashboard page.
-class DashboardPage extends StatefulWidget {
- DashboardPage({Key? key}) : super(key: key);
-
- @override
- _DashboardPageState createState() => _DashboardPageState();
-}
-
-class _DashboardPageState extends State<DashboardPage> {
- late Timer _timer;
-
- double speed = 20;
- // between 0 and 1.0
- double rpm = 0.2;
- // between 0 and 1.0
- double fuel = 0.2;
-
- @override
- void initState() {
- _timer = new Timer.periodic(
- Duration(milliseconds: 10),
- (Timer timer) {
- setState(() {
- double now = DateTime.now().millisecondsSinceEpoch / 2000;
- speed = 50 + 40 * sin(now);
- rpm = 0.5 + sin(now) / 3.0;
- fuel = 0.6 + cos(now) / 4.0;
- });
- },
- );
- // Animate the values for the demo.
- // Eventually, we will get the state of the car from the API.
- super.initState();
- }
-
- @override
- void dispose() {
- _timer.cancel();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- var sizeHelper = LayoutSizeHelper(context);
- return Container(
- decoration: BoxDecoration(
- gradient: LinearGradient(
- begin: Alignment.topRight,
- end: Alignment.bottomLeft,
- colors: [Colors.teal.shade900, Colors.grey.shade900])),
- constraints: BoxConstraints.expand(),
- alignment: Alignment.center,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- Column(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- Container(
- height: sizeHelper.largeIconSize * 1,
- width: sizeHelper.largeIconSize * 2,
- decoration: BoxDecoration(
- border: Border.all(
- color: Theme.of(context).primaryColorLight,
- width: sizeHelper.defaultBorder,
- ),
- borderRadius: BorderRadius.all(
- Radius.circular(sizeHelper.largeIconSize / 2.0))),
- child: Center(
- child: Text(
- '${speed.floor()} kpm',
- style: Theme.of(context).textTheme.headline2,
- ),
- ),
- ),
- _RPMWidget(rpm),
- _FuelWidget(fuel),
- ],
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Column(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- _TireWidget('Left front tire', 21, CrossAxisAlignment.end),
- _TireWidget('Left rear tire', 23, CrossAxisAlignment.end),
- ],
- ),
- Image.asset(
- 'images/HMI_Dashboard_Car_720.png',
- width: 2.0 * sizeHelper.largeIconSize,
- fit: BoxFit.contain,
- ),
- Column(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- _TireWidget('Right front tire', 21, CrossAxisAlignment.start),
- _TireWidget('Right rear tire', 23, CrossAxisAlignment.start),
- ],
- ),
- ],
- ),
- ],
- ),
- );
- }
-}
-
-// The RPM indicator.
-class _RPMWidget extends StatelessWidget {
- final double rpm;
-
- _RPMWidget(this.rpm);
-
- @override
- Widget build(BuildContext context) {
- var sizeHelper = LayoutSizeHelper(context);
- return Stack(
- alignment: Alignment.center,
- children: [
- Text(
- 'RPM',
- style: Theme.of(context).textTheme.headline4,
- ),
- Container(
- height: sizeHelper.largeIconSize * 1.5,
- width: sizeHelper.largeIconSize * 1.5,
- child: RotatedBox(
- quarterTurns: 2,
- child: CircularProgressIndicator(
- value: rpm,
- color: HSLColor.fromColor(Colors.redAccent)
- .withSaturation(rpm)
- .toColor(),
- strokeWidth: sizeHelper.largeIconSize / 2.0,
- semanticsLabel: 'RPM indicator',
- ),
- ),
- )
- ],
- );
- }
-}
-
-// The fuel indicator.
-class _FuelWidget extends StatelessWidget {
- final double fuel;
-
- _FuelWidget(this.fuel);
-
- @override
- Widget build(BuildContext context) {
- var sizeHelper = LayoutSizeHelper(context);
- return Row(
- children: [
- Container(
- height: sizeHelper.largeIconSize / 4.0,
- width: sizeHelper.largeIconSize / 2.0,
- child: Center(
- child: Text(
- 'Fuel',
- style: Theme.of(context).textTheme.headline4,
- ),
- ),
- ),
- Container(
- height: sizeHelper.largeIconSize / 4.0,
- width: sizeHelper.largeIconSize * 1.5,
- margin: EdgeInsets.fromLTRB(
- 0, sizeHelper.largePadding, 0, sizeHelper.largePadding),
- child: LinearProgressIndicator(
- value: fuel,
- color: HSLColor.fromColor(Colors.blueAccent)
- .withSaturation(fuel)
- .toColor(),
- semanticsLabel: 'RPM indicator',
- ),
- )
- ],
- );
- }
-}
-
-// The small indicator for the state of each tire.
-class _TireWidget extends StatelessWidget {
- final String label;
- final int value;
- final CrossAxisAlignment crossAlign;
-
- _TireWidget(this.label, this.value, this.crossAlign);
-
- @override
- Widget build(BuildContext context) {
- return Column(
- crossAxisAlignment: crossAlign,
- children: [
- Text(
- label,
- style: Theme.of(context).textTheme.headline6,
- ),
- Text(
- '$value PSI',
- style: Theme.of(context).textTheme.headline4,
- ),
- ],
- );
- }
-}
diff --git a/lib/page_hvac.dart b/lib/page_hvac.dart
deleted file mode 100644
index 65d77b6..0000000
--- a/lib/page_hvac.dart
+++ /dev/null
@@ -1,313 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_homescreen/homescreen_model.dart';
-import 'package:flutter_homescreen/layout_size_helper.dart';
-import 'package:flutter_homescreen/switchable_image.dart';
-import 'package:numberpicker/numberpicker.dart';
-import 'package:provider/provider.dart';
-
-// image assets
-const String LEFT_SEAT = 'images/HMI_HVAC_Left_Chair_ON.png';
-const String RIGHT_SEAT = 'images/HMI_HVAC_Right_Chair_ON.png';
-const String CIRCULATION = 'images/HMI_HVAC_Circulation_Active.png';
-const String AIRDOWN = 'images/HMI_HVAC_AirDown_Active.png';
-const String AIRUP = 'images/HMI_HVAC_AirUp_Active.png';
-const String FRONT = 'images/HMI_HVAC_Front_Active.png';
-const String REAR = 'images/HMI_HVAC_Rear_Active.png';
-
-// The page for heating, ventilation, and air conditioning.
-class HVACPage extends StatelessWidget {
- HVACPage({Key? key}) : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- var sizeHelper = LayoutSizeHelper(context);
-
- Widget fanSpeedControl = Container(
- padding: EdgeInsets.symmetric(
- vertical: sizeHelper.defaultPadding,
- horizontal: 3.0 * sizeHelper.defaultPadding,
- ),
- child: Row(
- children: [
- Expanded(
- flex: 3,
- child: HVACFanSpeed(),
- ),
- Expanded(
- flex: 1,
- child: Container(
- alignment: Alignment.centerLeft,
- child: Image.asset('images/HMI_HVAC_Fan_Icon.png',
- width: sizeHelper.defaultIconSize,
- height: sizeHelper.defaultIconSize,
- fit: BoxFit.contain)),
- ),
- ],
-
- mainAxisAlignment: MainAxisAlignment.start,
- mainAxisSize: MainAxisSize.max,
- crossAxisAlignment: CrossAxisAlignment.center,
- ),
- );
-
- Widget centerView = Container(
- padding: EdgeInsets.all(sizeHelper.defaultPadding),
- child: Column(
- children: [
- _HVACToggleButton(
- label: 'A/C',
- switchId: SwitchId.hvacAc,
- ),
- _HVACToggleButton(
- label: 'Auto',
- switchId: SwitchId.hvacAuto,
- ),
- _HVACToggleButton(
- imageAssetId: CIRCULATION,
- switchId: SwitchId.hvacCirculation,
- ),
- ],
- ),
- );
-
- Widget actions =
- Consumer<HomescreenModel>(builder: (context, model, child) {
- return Column(
- children: [
- _ActionButton(switchId: SwitchId.hvacAirDown, imageAssetId: AIRDOWN),
- SizedBox(height: sizeHelper.defaultPadding),
- _ActionButton(switchId: SwitchId.hvacAirUp, imageAssetId: AIRUP),
- SizedBox(height: sizeHelper.defaultPadding),
- _ActionButton(switchId: SwitchId.hvacFront, imageAssetId: FRONT),
- SizedBox(height: sizeHelper.defaultPadding),
- _ActionButton(switchId: SwitchId.hvacRear, imageAssetId: REAR),
- ],
- );
- });
-
- return Container(
- decoration: BoxDecoration(
- gradient: LinearGradient(
- begin: Alignment.topRight,
- end: Alignment.bottomLeft,
- colors: [Colors.blueGrey.shade900, Colors.grey.shade900])),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: <Widget>[
- fanSpeedControl,
- Row(children: [
- Expanded(
- flex: 1,
- child: _SeatButton(
- switchId: SwitchId.hvacLeftSeat,
- temperatureId: TemperatureId.leftSeat,
- imageAssetId: LEFT_SEAT,
- )),
- Expanded(flex: 1, child: centerView),
- Expanded(
- flex: 1,
- child: _SeatButton(
- switchId: SwitchId.hvacRigthSeat,
- temperatureId: TemperatureId.rightSeat,
- imageAssetId: RIGHT_SEAT,
- )),
- Expanded(flex: 1, child: actions)
- ])
- ],
- ));
- }
-}
-
-// The temperature selector.
-class _TemperatureSelector extends StatelessWidget {
- final TemperatureId temperatureId;
-
- _TemperatureSelector({Key? key, required this.temperatureId})
- : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- var sizeHelper = LayoutSizeHelper(context);
- return Consumer<HomescreenModel>(
- builder: (context, model, child) {
- return NumberPicker(
- value: model.getTemperature(temperatureId),
- minValue: 18,
- maxValue: 25,
- onChanged: (value) => model.setTemperature(temperatureId, value),
- textStyle: DefaultTextStyle.of(context).style.copyWith(
- color: Colors.teal.shade200,
- fontSize: sizeHelper.baseFontSize,
- ),
- selectedTextStyle: DefaultTextStyle.of(context).style.copyWith(
- fontSize: sizeHelper.baseFontSize * 1.5,
- ),
- itemHeight: sizeHelper.baseFontSize * 3,
- itemWidth: sizeHelper.baseFontSize * 6,
- );
- },
- );
- }
-}
-
-/// The fan speed control.
-class HVACFanSpeed extends StatelessWidget {
- const HVACFanSpeed({Key? key}) : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- return SliderTheme(
- data: SliderThemeData(
- thumbColor: Colors.greenAccent.shade700,
- activeTrackColor: Colors.greenAccent.shade700,
- inactiveTrackColor: Colors.blueGrey.shade200,
- ),
- child: Consumer<HomescreenModel>(
- builder: (context, model, child) {
- return Slider(
- value: model.fanSpeed,
- min: 0,
- max: 300,
- label: model.fanSpeed.round().toString(),
- onChanged: (double newValue) {
- model.fanSpeed = newValue;
- },
- );
- },
- ),
- );
- }
-}
-
-// the button to enable A/C on each seat
-class _SeatButton extends StatelessWidget {
- final SwitchId switchId;
- final TemperatureId temperatureId;
- final String imageAssetId;
-
- const _SeatButton({
- Key? key,
- required this.switchId,
- required this.temperatureId,
- required this.imageAssetId,
- }) : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- var sizeHelper = LayoutSizeHelper(context);
- return Container(
- padding: EdgeInsets.all(sizeHelper.defaultPadding),
- child: Column(
- children: [
- Consumer<HomescreenModel>(
- builder: (context, model, child) {
- return IconButton(
- onPressed: () => model.flipSwitch(switchId),
- iconSize: sizeHelper.largeIconSize,
- icon: SwitchableImage(
- value: model.getSwitchState(switchId),
- imageAssetId: imageAssetId,
- width: sizeHelper.largeIconSize,
- height: sizeHelper.largeIconSize,
- ),
- );
- },
- ),
- SizedBox(height: sizeHelper.defaultPadding),
- _TemperatureSelector(temperatureId: temperatureId),
- ],
- ),
- );
- }
-}
-
-// Each one of the large toggle buttons in the UI.
-class _HVACToggleButton extends StatelessWidget {
- final String? label;
- final String? imageAssetId;
- final SwitchId switchId;
-
- _HVACToggleButton(
- {Key? key, required this.switchId, this.label, this.imageAssetId})
- : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- var sizeHelper = LayoutSizeHelper(context);
- TextStyle buttonTextStyle = DefaultTextStyle.of(context).style.copyWith(
- fontSize: sizeHelper.baseFontSize,
- fontWeight: FontWeight.bold,
- );
- TextStyle unselectedButtonTextStyle = buttonTextStyle.copyWith(
- color: Colors.grey,
- fontWeight: FontWeight.normal,
- );
-
- return Container(
- width: sizeHelper.defaultButtonWidth,
- height: sizeHelper.defaultButtonHeight,
- margin: EdgeInsets.all(sizeHelper.defaultPadding),
- child: Consumer<HomescreenModel>(
- builder: (context, model, child) {
- return OutlinedButton(
- onPressed: () => model.flipSwitch(switchId),
- style: OutlinedButton.styleFrom(
- shape: RoundedRectangleBorder(
- borderRadius:
- BorderRadius.circular(sizeHelper.defaultButtonHeight / 4.0),
- ),
- side: BorderSide(
- width: sizeHelper.defaultBorder,
- color:
- model.getSwitchState(switchId) ? Colors.green : Colors.grey,
- style: BorderStyle.solid,
- ),
- ),
- child: (imageAssetId != null)
- ? SwitchableImage(
- value: model.getSwitchState(switchId),
- imageAssetId: imageAssetId ?? '',
- width: sizeHelper.defaultIconSize,
- height: sizeHelper.defaultIconSize,
- )
- : Text(
- label ?? '',
- style: model.getSwitchState(switchId)
- ? buttonTextStyle
- : unselectedButtonTextStyle,
- ),
- );
- },
- ),
- );
- }
-}
-
-// Each one of the small action buttons.
-class _ActionButton extends StatelessWidget {
- final SwitchId switchId;
- final String imageAssetId;
-
- const _ActionButton(
- {Key? key, required this.switchId, required this.imageAssetId})
- : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- var sizeHelper = LayoutSizeHelper(context);
- return Consumer<HomescreenModel>(
- builder: (context, model, child) {
- return IconButton(
- onPressed: () => model.flipSwitch(switchId),
- iconSize: sizeHelper.defaultIconSize,
- icon: SwitchableImage(
- value: model.getSwitchState(switchId),
- imageAssetId: imageAssetId,
- width: sizeHelper.defaultIconSize,
- height: sizeHelper.defaultIconSize,
- ),
- );
- },
- );
- }
-}
diff --git a/lib/vehicle-signals/viss_config.dart b/lib/vehicle-signals/viss_config.dart
new file mode 100644
index 0000000..c2be5ee
--- /dev/null
+++ b/lib/vehicle-signals/viss_config.dart
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Apache-2.0
+//import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter_homescreen/config.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+//import 'package:http/http.dart' as http;
+
+final sockConnectprovider = FutureProvider.family<WebSocket, HttpClient>(
+ (ref, client) => connect(client, ref));
+
+Future<HttpClient> initializeClient() async {
+ SecurityContext ctx = SecurityContext.defaultContext;
+
+ HttpClient client = HttpClient(context: ctx)
+ ..findProxy = null
+ ..badCertificateCallback = (cert, host, port) {
+ return true;
+ };
+ return client;
+}
+
+Future<WebSocket> connect(HttpClient client, ref) async {
+ final config = ref.read(ConfigStateprovider);
+ WebSocket socket = await WebSocket.connect(
+ "wss://${config.hostname}:${config.port}",
+ customClient: client);
+ return socket;
+}
diff --git a/lib/vehicle-signals/viss_connected_widget.dart b/lib/vehicle-signals/viss_connected_widget.dart
new file mode 100644
index 0000000..dd3e4aa
--- /dev/null
+++ b/lib/vehicle-signals/viss_connected_widget.dart
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: Apache-2.0
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter_homescreen/vehicle-signals/viss_config.dart';
+import 'package:flutter_homescreen/vehicle-signals/viss_methods.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_homescreen/vehicle-signals/vss_providers.dart';
+
+class VISServerConnectedWidget extends ConsumerStatefulWidget {
+ const VISServerConnectedWidget(
+ {Key? key, required this.client, required this.socket})
+ : super(key: key);
+ final WebSocket socket;
+ final HttpClient client;
+
+ @override
+ ConsumerState<VISServerConnectedWidget> createState() =>
+ _VISServerConnectedWidgetState();
+}
+
+class _VISServerConnectedWidgetState
+ extends ConsumerState<VISServerConnectedWidget> {
+ late Timer _timer;
+
+ void _updateSocket() {
+ ref.read(VISServerSocketProvider.notifier).update(widget.socket);
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ VISS.init(widget.socket, ref);
+ Future.delayed(Duration.zero, () => _updateSocket());
+ _timer = Timer.periodic(const Duration(seconds: 2), (timer) {
+ if (widget.socket.readyState == 3) {
+ ref.refresh(sockConnectprovider(widget.client));
+ }
+ });
+ WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+ widget.socket.listen(
+ (data) {
+ VISS.parseData(ref, data);
+ },
+ onError: (e, stk) {
+ print(e.toString());
+ ref.refresh(sockConnectprovider(widget.client));
+ },
+ );
+ });
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ _timer.cancel();
+ widget.socket.close(786887, "Connection lost with server!");
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
diff --git a/lib/vehicle-signals/viss_connection_widget.dart b/lib/vehicle-signals/viss_connection_widget.dart
new file mode 100644
index 0000000..a2abee7
--- /dev/null
+++ b/lib/vehicle-signals/viss_connection_widget.dart
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: Apache-2.0
+import 'dart:io';
+import 'package:flutter_homescreen/vehicle-signals/viss_config.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'viss_connected_widget.dart';
+
+class VISServerConnectionWidget extends ConsumerWidget {
+ VISServerConnectionWidget({Key? key, required this.client}) : super(key: key);
+ final HttpClient client;
+ late WebSocket socket;
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final sockConnect = ref.watch(sockConnectprovider(client));
+
+ return sockConnect.when(
+ data: (socket) {
+ this.socket = socket;
+ this.socket.pingInterval = const Duration(seconds: 2);
+ return VISServerConnectedWidget(client: client, socket: this.socket);
+ },
+ error: (e, stk) {
+ print(e);
+ ref.refresh(sockConnectprovider(client));
+ return Container(
+ child: const Text('Connection Error',
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ color: Colors.white)));
+ },
+ loading: () => Container(
+ child: Text('Connected',
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ color: Colors.white))));
+ }
+}
diff --git a/lib/vehicle-signals/viss_methods.dart b/lib/vehicle-signals/viss_methods.dart
new file mode 100644
index 0000000..8adcc80
--- /dev/null
+++ b/lib/vehicle-signals/viss_methods.dart
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: Apache-2.0
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter_homescreen/vehicle-signals/vss_providers.dart';
+import 'package:flutter_homescreen/vehicle-signals/vss_path.dart';
+import 'package:flutter_homescreen/config.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+class VISS {
+ static const requestId = "test-id";
+
+ static void init(WebSocket socket, WidgetRef ref) {
+ authorize(socket, ref);
+ subscribe(socket, ref, VSSPath.vehicleMediaVolume);
+ }
+
+ static void update(WebSocket socket, WidgetRef ref) {
+ get(socket, ref, VSSPath.vehicleMediaVolume);
+ }
+
+ static void authorize(WebSocket socket, WidgetRef ref) {
+ final config = ref.read(ConfigStateprovider);
+
+ Map<String, dynamic> map = {
+ "action": "authorize",
+ "tokens": config.kuksaAuthToken,
+ "requestId": requestId
+ };
+ socket.add(jsonEncode(map));
+ }
+
+ static void get(WebSocket socket, WidgetRef ref, String path) {
+ final config = ref.read(ConfigStateprovider);
+
+ Map<String, dynamic> map = {
+ "action": "get",
+ "tokens": config.kuksaAuthToken,
+ "path": path,
+ "requestId": requestId
+ };
+ socket.add(jsonEncode(map));
+ }
+
+ static void set(WebSocket socket, WidgetRef ref, String path, String value) {
+ final config = ref.read(ConfigStateprovider);
+ Map<String, dynamic> map = {
+ "action": "set",
+ "tokens": config.kuksaAuthToken,
+ "path": path,
+ "requestId": requestId,
+ "value": value
+ };
+ socket.add(jsonEncode(map));
+ }
+
+ static void subscribe(WebSocket socket, WidgetRef ref, String path) {
+ final config = ref.read(ConfigStateprovider);
+
+ Map<String, dynamic> map = {
+ "action": "subscribe",
+ "tokens": config.kuksaAuthToken,
+ "path": path,
+ "requestId": requestId
+ };
+ socket.add(jsonEncode(map));
+ }
+
+ static void parseData(WidgetRef ref, String data) {
+ Map<String, dynamic> dataMap = jsonDecode(data);
+ if (dataMap["action"] == "subscription" || dataMap["action"] == "get") {
+ if (dataMap.containsKey("data")) {
+ if ((dataMap["data"] as Map<String, dynamic>).containsKey("dp") &&
+ (dataMap["data"] as Map<String, dynamic>).containsKey("path")) {
+ String path = dataMap["data"]["path"];
+ Map<String, dynamic> dp = dataMap["data"]["dp"];
+ if (dp.containsKey("value")) {
+ if (dp["value"] != "---") {
+ switch (path) {
+ case VSSPath.vehicleMediaVolume:
+ ref
+ .read(vehicleSignalMediaVolumeProvider.notifier)
+ .update(volume: dp["value"]);
+ break;
+ case VSSPath.vehicleInsideTemperature:
+ ref
+ .read(vehicleSignalInsideTempProvider.notifier)
+ .update(temp: dp["value"]);
+ break;
+ case VSSPath.vehicleOutsideTemperature:
+ ref
+ .read(vehicleSignalOutsideTempProvider.notifier)
+ .update(temp: dp["value"]);
+ break;
+ default:
+ break;
+ }
+ } else {
+ print("ERROR: Invalid VIS response, data not available");
+ }
+ } else {
+ print("ERROR: Invalid VIS response, no 'value' key");
+ }
+ } else if ((!dataMap["data"] as Map<String, dynamic>)
+ .containsKey("path")) {
+ print("ERROR: Invalid VIS response, no 'path' key");
+ } else if ((dataMap["data"] as Map<String, dynamic>)
+ .containsKey("dp")) {
+ print("ERROR: Invalid VIS response, no 'dp' key");
+ }
+ } else {
+ print("ERROR: Invalid VIS response, no 'data' key");
+ }
+ }
+ }
+}
diff --git a/lib/vehicle-signals/vss_path.dart b/lib/vehicle-signals/vss_path.dart
new file mode 100644
index 0000000..3bfc9c1
--- /dev/null
+++ b/lib/vehicle-signals/vss_path.dart
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: Apache-2.0
+class VSSPath {
+ static const String vehicleSpeed = "Vehicle.Speed";
+
+ static const String vehicleEngineRPM =
+ "Vehicle.Powertrain.CombustionEngine.Speed";
+
+ static const String vehicleFuelLevel = "Vehicle.Powertrain.FuelSystem.Level";
+
+ static const String vehicleInsideTemperature =
+ "Vehicle.Cabin.HVAC.AmbientAirTemperature";
+
+ static const String vehicleOutsideTemperature =
+ "Vehicle.Exterior.AirTemperature";
+
+ static const String vehicleFrontLeftTire =
+ "Vehicle.Chassis.Axle.Row1.Wheel.Left.Tire.Pressure";
+
+ static const String vehicleFrontRightTire =
+ "Vehicle.Chassis.Axle.Row1.Wheel.Right.Tire.Pressure";
+
+ static const String vehicleRearLeftTire =
+ "Vehicle.Chassis.Axle.Row2.Wheel.Left.Tire.Pressure";
+
+ static const String vehicleRearRightTire =
+ "Vehicle.Chassis.Axle.Row2.Wheel.Right.Tire.Pressure";
+
+ static const String vehicleIsChildLockActiveLeft =
+ "Vehicle.Cabin.Door.Row2.Left.IsChildLockActive";
+
+ static const String vehicleIsChildLockActiveRight =
+ "Vehicle.Cabin.Door.Row2.Right.IsChildLockActive";
+
+ static const String vehicleCurrentLongitude =
+ "Vehicle.CurrentLocation.Longitude";
+
+ static const String vehicleCurrentLatitude =
+ "Vehicle.CurrentLocation.Latitude";
+
+ static const String vehicleFuelRate = "Vehicle.OBD.FuelRate";
+
+ static const String vehicleMediaVolume =
+ "Vehicle.Cabin.Infotainment.Media.Volume";
+}
diff --git a/lib/vehicle-signals/vss_providers.dart b/lib/vehicle-signals/vss_providers.dart
new file mode 100644
index 0000000..630a273
--- /dev/null
+++ b/lib/vehicle-signals/vss_providers.dart
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: Apache-2.0
+//import 'dart:ffi';
+import 'dart:io';
+import 'package:meta/meta.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+@immutable
+class VISServerSocket {
+ const VISServerSocket({required this.socket});
+
+ final WebSocket? socket;
+
+ VISServerSocket copyWith({WebSocket? socket}) {
+ return VISServerSocket(socket: socket ?? this.socket);
+ }
+}
+
+class VISServerSocketNotifier extends StateNotifier<VISServerSocket> {
+ VISServerSocketNotifier() : super(_initialValue);
+
+ static final VISServerSocket _initialValue = VISServerSocket(socket: null);
+
+ void update(WebSocket socket) {
+ state = state.copyWith(socket: socket);
+ }
+}
+
+final VISServerSocketProvider =
+ StateNotifierProvider<VISServerSocketNotifier, VISServerSocket>((ref) {
+ return VISServerSocketNotifier();
+});
+
+// Media Volume
+
+@immutable
+class VehicleSignalMediaVolume {
+ const VehicleSignalMediaVolume({required this.volume});
+
+ final int volume;
+
+ VehicleSignalMediaVolume copyWith({int? volume}) {
+ return VehicleSignalMediaVolume(volume: volume ?? this.volume);
+ }
+}
+
+class VehicleSignalMediaVolumeNotifier
+ extends StateNotifier<VehicleSignalMediaVolume> {
+ VehicleSignalMediaVolumeNotifier() : super(_initialValue);
+
+ static final VehicleSignalMediaVolume _initialValue =
+ VehicleSignalMediaVolume(volume: 50);
+
+ void update({int? volume}) {
+ int? n = volume;
+ if (n != null) {
+ n = n.toUnsigned(8);
+ if (n > 100) n = 100;
+ }
+ state = state.copyWith(volume: n);
+ }
+}
+
+//final vehicleSignalMediaVolumeProvider = StateNotifierProvider<
+// VehicleSignalMediaVolumeNotifier, VehicleSignalMediaVolume>((ref) {
+// return VehicleSignalMediaVolumeNotifier();
+//});
+
+final vehicleSignalMediaVolumeProvider = StateNotifierProvider<
+ VehicleSignalMediaVolumeNotifier,
+ VehicleSignalMediaVolume>((ref) => VehicleSignalMediaVolumeNotifier());
+
+// Inside Temperature
+
+@immutable
+class VehicleSignalInsideTemp {
+ const VehicleSignalInsideTemp({required this.temp});
+
+ final double temp;
+
+ VehicleSignalInsideTemp copyWith({double? temp}) {
+ return VehicleSignalInsideTemp(temp: temp ?? this.temp);
+ }
+}
+
+class VehicleSignalInsideTempNotifier
+ extends StateNotifier<VehicleSignalInsideTemp> {
+ VehicleSignalInsideTempNotifier() : super(_initialValue);
+
+ static final VehicleSignalInsideTemp _initialValue =
+ VehicleSignalInsideTemp(temp: 25);
+
+ void update({double? temp}) {
+ state = state.copyWith(temp: temp);
+ }
+}
+
+final vehicleSignalInsideTempProvider = StateNotifierProvider<
+ VehicleSignalInsideTempNotifier, VehicleSignalInsideTemp>(
+ (ref) => VehicleSignalInsideTempNotifier(),
+);
+
+// Outside Temperature
+
+@immutable
+class VehicleSignalOutsideTemp {
+ const VehicleSignalOutsideTemp({required this.temp});
+
+ final double temp;
+
+ VehicleSignalOutsideTemp copyWith({double? temp}) {
+ return VehicleSignalOutsideTemp(temp: temp ?? this.temp);
+ }
+}
+
+class VehicleSignalOutsideTempNotifier
+ extends StateNotifier<VehicleSignalOutsideTemp> {
+ VehicleSignalOutsideTempNotifier() : super(_initialValue);
+
+ static final VehicleSignalOutsideTemp _initialValue =
+ VehicleSignalOutsideTemp(temp: 32);
+
+ void update({double? temp}) {
+ state = state.copyWith(temp: temp);
+ }
+}
+
+final vehicleSignalOutsideTempProvider = StateNotifierProvider<
+ VehicleSignalOutsideTempNotifier, VehicleSignalOutsideTemp>(
+ (ref) => VehicleSignalOutsideTempNotifier(),
+);
diff --git a/lib/volume_slider.dart b/lib/volume_slider.dart
new file mode 100644
index 0000000..0665491
--- /dev/null
+++ b/lib/volume_slider.dart
@@ -0,0 +1,50 @@
+import 'dart:io';
+import 'package:async/async.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_homescreen/vehicle-signals/vss_providers.dart';
+import 'package:flutter_homescreen/vehicle-signals/vss_path.dart';
+import 'package:flutter_homescreen/vehicle-signals/viss_methods.dart';
+
+class VolumeSlider extends ConsumerWidget {
+ final Color thumbColor;
+ final Color activeColor;
+ final Color inactiveColor;
+ final RestartableTimer? activityTimer;
+
+ VolumeSlider(
+ {Key? key,
+ required this.thumbColor,
+ required this.activeColor,
+ required this.inactiveColor,
+ required this.activityTimer})
+ : super(key: key);
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final signal = ref.watch(vehicleSignalMediaVolumeProvider);
+ return Slider(
+ thumbColor: thumbColor,
+ activeColor: activeColor,
+ inactiveColor: inactiveColor,
+ value: signal.volume.toDouble(),
+ max: 100,
+ label: "Volume",
+ onChangeStart: (value) {
+ activityTimer?.cancel();
+ },
+ onChangeEnd: (value) {
+ activityTimer?.reset();
+ },
+ onChanged: (value) {
+ ref
+ .read(vehicleSignalMediaVolumeProvider.notifier)
+ .update(volume: value.toInt());
+ WebSocket? s = ref.read(VISServerSocketProvider).socket;
+ if (s != null) {
+ VISS.set(
+ s, ref, VSSPath.vehicleMediaVolume, value.toInt().toString());
+ }
+ });
+ }
+}