flutter_hue 2.0.0-beta.3
flutter_hue: ^2.0.0-beta.3 copied to clipboard
An SDK designed for the Flutter framework that enables developers to easily integrate Philips Hue smart devices into their applications.
example/lib/main.dart
import 'dart:async';
import 'package:example/stream_demos/stream_demos_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hue/flutter_hue.dart';
import 'package:uni_links/uni_links.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
static const double padding = 15.0;
/// Whether or not the page is loading some async action.
bool isLoading = false;
/// The IP address of the bridges on the network.
final List<String> bridgeIps = [];
/// Controls the bridge discovery process.
final DiscoveryTimeoutController timeoutController =
DiscoveryTimeoutController(timeoutSeconds: 25);
/// Cancels the "first contact" action.
VoidCallback? onContactCancel;
/// The bridge that [firstContact] decided to connect with.
Bridge? bridge;
/// All of the Philips Hue resources connected to [bridge].
HueNetwork? hueNetwork;
/// The light that is being worked with in the "writing data" section.
Light? light;
/// Watches for deep links.
late final StreamSubscription deepLinkStream;
@override
void initState() {
super.initState();
deepLinkStream = uriLinkStream.listen(
(Uri? uri) {
if (uri == null) return;
final int start = uri.toString().indexOf("?");
String queryParams = uri.toString().substring(start);
Uri truncatedUri = Uri.parse(queryParams);
try {
final String? pkce = truncatedUri.queryParameters[ApiFields.pkce];
final String? code = truncatedUri.queryParameters[ApiFields.code];
final String? resState =
truncatedUri.queryParameters[ApiFields.state];
// Handle Flutter Hue deep link
if (pkce != null && code != null && resState != null) {
String stateSecret;
if (resState.contains("-")) {
stateSecret = resState.substring(0, resState.indexOf("-"));
} else {
stateSecret = resState;
}
TokenRepo.fetchRemoteToken(
clientId: "[clientId]", // TODO: Replace with your client ID
clientSecret:
"[clientSecret]", // TODO: Replace with your client secret
pkce: pkce,
code: code,
stateSecret: stateSecret,
decrypter: (ciphertext) =>
ciphertext.substring(4, ciphertext.length - 4),
);
}
} catch (_) {
// Do nothing
}
},
);
// Initialize Flutter Hue and keep all of the locally stored data up to
// date.
FlutterHueMaintenanceRepo.maintain(
clientId: "[clientId]", // TODO: Replace with your client ID
clientSecret: "[clientSecret]", // TODO: Replace with your client secret
redirectUri: "flutterhue://auth",
deviceName: "TestDevice",
stateEncrypter: (plaintext) => "abcd${plaintext}1234",
);
}
@override
void dispose() {
deepLinkStream.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: doSomething,
icon: const Icon(Icons.science),
),
title: const Text("Flutter Hue"),
actions: isLoading
? [
const Padding(
padding: EdgeInsets.only(right: padding),
child: Row(
children: [
Text("Loading... "),
Icon(Icons.query_builder),
],
),
),
]
: null,
),
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: padding),
sectionHeader("Getting Started"),
// DISCOVER BRIDGES
Column(
children: [
ElevatedButton(
onPressed: discoverBridges,
child: const Text("Discover Bridges"),
),
Visibility(
visible: bridgeIps.isNotEmpty,
child: TextButton(
onPressed: () => showIps(context),
child: Text("Found ${bridgeIps.length} bridge IP"
"${bridgeIps.length == 1 ? "" : "s"}"),
),
),
],
),
const SizedBox(height: padding * 2),
// FIRST CONTACT
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: bridgeIps.isEmpty ? null : () => firstContact(),
child: const Text("First Contact"),
),
const SizedBox(width: 11),
ElevatedButton(
onPressed: onContactCancel,
child: const Text("Cancel"),
),
],
),
const SizedBox(height: padding * 2),
// ESTABLISH REMOTE CONTACT
ElevatedButton(
onPressed: bridge == null ? null : remoteContact,
child: const Text("Establish Remote Contact"),
),
const SizedBox(height: padding * 2),
sectionHeader("Reading Data"),
// FETCH NETWORK
ElevatedButton(
onPressed: bridge == null ? null : fetchNetwork,
child: const Text("Fetch Network"),
),
const SizedBox(height: padding * 2),
// FETCH BRIDGE
ElevatedButton(
onPressed: bridge == null ? null : fetchBridge,
child: const Text("Fetch Bridge"),
),
const SizedBox(height: padding * 2),
// FETCH LIGHT
ElevatedButton(
onPressed: bridge == null ? null : fetchLight,
child: const Text("Fetch Light"),
),
const SizedBox(height: padding * 2),
sectionHeader("Writing Data"),
// IDENTIFY LIGHT
ElevatedButton(
onPressed: light == null ? null : identifyLight,
child: const Text("Identify Light"),
),
const SizedBox(height: padding * 2),
// TOGGLE LIGHT ON/OFF
ElevatedButton(
onPressed: light == null ? null : toggleLight,
child: const Text("Toggle Light on/off"),
),
const SizedBox(height: padding * 2),
// LIGHT COLORS
Padding(
padding: const EdgeInsets.symmetric(horizontal: padding),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// RED
ElevatedButton(
onPressed: light == null ? null : () => colorLight("red"),
child: const Text("Red"),
),
// GREEN
ElevatedButton(
onPressed:
light == null ? null : () => colorLight("green"),
child: const Text("Green"),
),
// BLUE
ElevatedButton(
onPressed:
light == null ? null : () => colorLight("blue"),
child: const Text("Blue"),
),
// WHITE
ElevatedButton(
onPressed:
light == null ? null : () => colorLight("white"),
child: const Text("White"),
),
],
),
),
const SizedBox(height: padding * 2),
sectionHeader(
"Entertainment Streaming",
GestureDetector(
onTap: (hueNetwork == null || !isStreaming)
? null
: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) {
return StreamDemosScreen(
entertainmentConfiguration: hueNetwork!
.entertainmentConfigurations.first,
);
},
),
);
},
child: Text(
"more >",
style: TextStyle(
color: (hueNetwork == null || !isStreaming)
? Colors.grey
: Colors.blue,
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: padding),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// STREAM 1
ElevatedButton(
onPressed:
hueNetwork == null ? null : () => startStreaming(1),
child: const Text('Stream 1'),
),
// STREAM 2
ElevatedButton(
onPressed:
hueNetwork == null ? null : () => startStreaming(2),
child: const Text('Stream 2'),
),
// STREAM 3
ElevatedButton(
onPressed:
hueNetwork == null ? null : () => startStreaming(3),
child: const Text('Stream 3'),
),
],
),
),
const SizedBox(height: padding * 2),
// STOP STREAMING
ElevatedButton(
onPressed:
(hueNetwork == null || !isStreaming) ? null : stopStreaming,
child: const Text("Stop Streaming"),
),
const SizedBox(height: padding),
],
),
),
),
);
}
/// The titles and dividers that separate each group of buttons.
Widget sectionHeader(String title, [Widget? actionBtn]) {
return Column(
children: [
Row(
children: [
const SizedBox(width: padding),
Text(
title,
style: const TextStyle(
fontSize: padding,
fontWeight: FontWeight.bold,
),
),
if (actionBtn != null) const Spacer(),
if (actionBtn != null) actionBtn,
if (actionBtn != null) const SizedBox(width: padding),
],
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: padding),
child: Divider(thickness: 2.0),
),
],
);
}
/// Show the IP addresses of the bridges that have been found on the network.
void showIps(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Bridge IP"),
content: SingleChildScrollView(
child: ListBody(
children: bridgeIps.map((ip) => Text(ip)).toList(),
),
),
actions: [
TextButton(
child: const Text("Ok"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
Future<void> doSomething() async {
setState(() {
isLoading = true;
});
// ignore: avoid_print
print('Doing something...');
// TODO: You can do your experiments easily here.
// ignore: avoid_print
print('Done!');
setState(() {
isLoading = false;
});
}
/// Searches the network for bridges.
///
/// If any are found, their IP addresses are placed in the [bridgeIps] list.
Future<void> discoverBridges() async {
setState(() {
isLoading = true;
});
List<String> bridges = await BridgeDiscoveryRepo.discoverBridges();
setState(() {
bridgeIps.addAll(bridges);
isLoading = false;
});
}
/// For the simplicity of this demo, this method only looks at the first
/// bridge from the [bridgeIps] list.
///
/// It attempts to establish contact with the bridge. This is when the user
/// needs to press the button on their bridge.
Future<void> firstContact() async {
setState(() {
onContactCancel = () => timeoutController.cancelDiscovery = true;
isLoading = true;
});
bridge = await BridgeDiscoveryRepo.firstContact(
bridgeIpAddr: bridgeIps.first,
controller: timeoutController,
);
setState(() {
onContactCancel = null;
isLoading = false;
});
}
/// Establishes remote contact with the bridge.
Future<void> remoteContact() async {
setState(() {
isLoading = true;
});
await BridgeDiscoveryRepo.remoteAuthRequest(
clientId: "[clientId]", // TODO: Replace with your client ID
redirectUri: "flutterhue://auth",
deviceName: "TestDevice",
encrypter: (plaintext) => "abcd${plaintext}1234",
);
setState(() {
isLoading = false;
});
}
/// Fetches all of the resources that are attached to [bridge].
Future<void> fetchNetwork() async {
setState(() {
isLoading = true;
});
hueNetwork = HueNetwork(bridges: [bridge!]);
await hueNetwork?.fetchAll();
try {
light = hueNetwork!.lights.first;
} catch (_) {
// Do nothing
}
setState(() {
isLoading = false;
});
}
/// This does nothing for the demo other than to show the code that is used to
/// fetch a bridge object from JSON.
Future<void> fetchBridge() async {
setState(() {
isLoading = true;
});
final List<Map<String, dynamic>>? res =
await bridge!.getResource(ResourceType.bridge);
try {
// ignore: unused_local_variable
Bridge myBridge = Bridge.fromJson(res?.first ?? {});
// Shows a way to display the info in these objects.
// log("Bridge Json - ${JsonTool.writeJson(res?.first ?? {})}");
// log("Bridge Object - ${JsonTool.writeJson(myBridge.toJson(optimizeFor: OptimizeFor.dontOptimize))}");
} catch (_) {
// res list was empty
}
setState(() {
isLoading = false;
});
}
/// This does nothing for the demo other than to show the code that is used to
/// fetch a light object from JSON.
Future<void> fetchLight() async {
setState(() {
isLoading = true;
});
final Map<String, dynamic>? res =
(await bridge!.getResource(ResourceType.light))?.first;
// ignore: unused_local_variable
Light light = Light.fromJson(res ?? {});
setState(() {
isLoading = false;
});
}
/// Causes the light to "breath" to let the user know which light they are
/// working with.
Future<void> identifyLight() async {
setState(() {
isLoading = true;
});
Device lightDevice;
try {
lightDevice = hueNetwork!.devices
// ignore: deprecated_member_use
.firstWhere((device) => device.metadata.name == light!.metadata.name);
} catch (_) {
return;
}
lightDevice.identifyAction = "identify";
await bridge!.put(lightDevice);
setState(() {
isLoading = false;
});
}
/// Toggles [light] on and off.
Future<void> toggleLight() async {
setState(() {
isLoading = true;
});
bool isOn = light!.isOn;
light!.on.isOn = !isOn;
await bridge!.put(light!);
setState(() {
isLoading = false;
});
}
/// Changes the color of [light].
Future<void> colorLight(String color) async {
setState(() {
isLoading = true;
});
double x;
double y;
if (color == "red") {
x = 0.6718;
y = 0.3184;
} else if (color == "green") {
x = 0.2487;
y = 0.6923;
} else if (color == "blue") {
x = 0.1121;
y = 0.1139;
} else {
x = 0.3127;
y = 0.3127;
}
light = light!
.copyWith(color: light!.color.copyWith(xy: LightColorXy(x: x, y: y)));
await bridge!.put(light!);
setState(() {
isLoading = false;
});
}
/// Whether or not the entertainment streaming process is currently active.
bool get isStreaming =>
_isStreamingPattern1 || _isStreamingPattern2 || _isStreamingPattern3;
/// Starts the entertainment streaming process.
///
/// The `pattern` parameter determines which pattern to use. The patterns are
/// as follows:
/// * `1`: Toggle 1 light between red and blue colors.
/// * `2`: Gently fade 1 light between red and blue colors.
/// * `3`: Toggles 2 lights between white and off, as if the light is jumping
/// back and forth between the two lights.
Future<void> startStreaming(int pattern) async {
setState(() {
isLoading = true;
});
try {
if (!isStreaming) {
final bool didStart = await hueNetwork!
.entertainmentConfigurations.first
.startStreaming(bridge!);
if (!didStart) throw "Failed to start stream";
}
// Clear out the stream queue before starting a new stream.
hueNetwork!.entertainmentConfigurations.first.flushStreamQueue();
if (pattern == 1) {
await _startStreaming1();
} else if (pattern == 2) {
await _startStreaming2();
} else if (pattern == 3) {
await _startStreaming3();
} else {
// ignore: avoid_print
print('Invalid pattern');
}
} catch (e) {
// ignore: avoid_print
print('Error starting stream: $e');
}
setState(() {
isLoading = false;
});
}
/// Whether or not the entertainment streaming process is currently active and
/// using pattern 1.
bool _isStreamingPattern1 = false;
/// Starts the entertainment streaming process, with streaming pattern 1.
///
/// Toggle 1 light between red and blue colors.
///
/// This should cause one light to alternate between red and blue.
///
/// The light will stay red or blue for 500ms then switch. This will
/// happen for 5 seconds, then it should stop on blue.
///
/// Since blue is the last state, it will still be streaming blue so
/// the bridge doesn't drop the connection due to inactivity.
Future<void> _startStreaming1() async {
_isStreamingPattern1 = true;
_isStreamingPattern2 = false;
_isStreamingPattern3 = false;
final ColorXy red = ColorXy.fromRgb(255, 0, 0, 1.0);
final ColorXy blue = ColorXy.fromRgb(0, 0, 255, 1.0);
final EntertainmentStreamCommand command1 = EntertainmentStreamCommand(
channel: 0,
color: red,
waitAfterAnimation: const Duration(milliseconds: 500),
);
final EntertainmentStreamCommand command2 = EntertainmentStreamCommand(
channel: 0,
color: blue,
waitAfterAnimation: const Duration(milliseconds: 500),
);
for (int i = 0; i < 5; i++) {
// IMPORTANT NOTE: The copy method is used here to ensure that the
// same command isn't added to the queue multiple times. If the
// same command is added multiple times, the bridge will only
// execute the command once.
hueNetwork!.entertainmentConfigurations.first.addAllToStreamQueue(
[command1.copy(), command2.copy()],
);
}
}
/// Whether or not the entertainment streaming process is currently active and
/// using pattern 2.
bool _isStreamingPattern2 = false;
/// Starts the entertainment streaming process, with streaming pattern 2.
///
/// Gently fade 1 light between red and blue colors.
///
/// This should cause one light to fade between red and blue.
///
/// The light will fade from red to blue over 500ms, then fade back to red
/// over 500ms. This will happen for 5 seconds, then it should stop on blue.
///
/// Since blue is the last state, it will still be streaming blue so
/// the bridge doesn't drop the connection due to inactivity.
Future<void> _startStreaming2() async {
_isStreamingPattern1 = false;
_isStreamingPattern2 = true;
_isStreamingPattern3 = false;
final ColorXy red = ColorXy.fromRgb(255, 0, 0, 1.0);
final ColorXy blue = ColorXy.fromRgb(0, 0, 255, 1.0);
final EntertainmentStreamCommand command1 = EntertainmentStreamCommand(
channel: 0,
color: red,
animationDuration: const Duration(milliseconds: 500),
animationType: AnimationType.ease,
);
final EntertainmentStreamCommand command2 = EntertainmentStreamCommand(
channel: 0,
color: blue,
animationDuration: const Duration(milliseconds: 500),
animationType: AnimationType.ease,
);
// This should cause one lights to alternate between red and blue.
//
// They will stay red or blue for 500ms then switch. This will
// happen for 5 seconds, then it should stop on blue.
//
// Since blue is the last state, it will still be streaming blue so
// the bridge doesn't drop the connection due to inactivity.
for (int i = 0; i < 5; i++) {
// IMPORTANT NOTE: The copy method is used here to ensure that the
// same command isn't added to the queue multiple times. If the
// same command is added multiple times, the bridge will only
// execute the command once.
hueNetwork!.entertainmentConfigurations.first.addAllToStreamQueue(
[command1.copy(), command2.copy()],
);
}
}
/// Whether or not the entertainment streaming process is currently active and
/// using pattern 3.
bool _isStreamingPattern3 = false;
/// Starts the entertainment streaming process, with streaming pattern 3.
///
/// Toggles 2 lights between white and off, as if the light is jumping back
/// and forth between the two lights.
///
/// One light will stay white for 500ms, then turn off for 500ms. The other
/// light will do the opposite. This will happen continuously until the user
/// stops the stream.
Future<void> _startStreaming3() async {
_isStreamingPattern1 = false;
_isStreamingPattern2 = false;
_isStreamingPattern3 = true;
final ColorXy white = ColorXy.fromRgb(255, 255, 255, 0.5);
final ColorXy off = ColorXy.fromRgb(0, 0, 0, 0.0);
// Continuously alternate between white and off for 500ms each.
Timer.periodic(
const Duration(milliseconds: 500),
(timer) {
// Turn the timer off when the user stops the stream.
if (!_isStreamingPattern3) {
timer.cancel();
return;
}
hueNetwork!.entertainmentConfigurations.first.addAllToStreamQueue(
[
EntertainmentStreamCommand(
channel: 0,
color: (timer.tick - 1) % 2 == 0 ? white : off,
waitAfterAnimation: const Duration(milliseconds: 500),
),
EntertainmentStreamCommand(
channel: 1,
color: (timer.tick - 1) % 2 == 0 ? off : white,
waitAfterAnimation: const Duration(milliseconds: 500),
),
],
);
},
);
}
/// Stops the entertainment streaming process.
Future<void> stopStreaming() async {
setState(() {
isLoading = true;
});
bool isStreaming = this.isStreaming;
try {
await hueNetwork!.entertainmentConfigurations.first
.stopStreaming(bridge!)
.then(
(value) {
if (value) {
isStreaming = false;
}
},
);
} catch (e) {
// ignore: avoid_print
print('Error stopping stream: $e');
}
setState(() {
if (!isStreaming) {
_isStreamingPattern1 = false;
_isStreamingPattern2 = false;
_isStreamingPattern3 = false;
}
isLoading = false;
});
}
}