In my previous post I showed how a Philips Hue bridge can be integrated with Home Assistant for controlling lights with automation. The Philips Hue bridge has an easy-to-use REST API so you can make your own integration on any platform that supports http. In this post I will describe how I have created a React web application for controlling my lights via the Philips Hue api.
About Philips Hue
See my previous post for more information on the Philips Hue system:
Philips Hue and Home Assistant
About React
React is a popular open-source JavaScript library (originally from Facebook) that lets you create responsive html/JavaScript user interfaces with structured and composable components. React’s rendering is very efficient as the React engine uses a virtual DOM for calculating what parts in the real DOM that need to be updated. React supports development with modern JavaScript (ES6 and JSX) and you normally use tools like webpack and babel for transpiling ES6 into a ES5 JavaScript bundle when you deploy your application so that the code can be executed on all modern browsers in an easy way.
Create-react-app
React and modern JavaScript is great, but setting up a proper transpiling- and bundling environment with webpack and babel from scratch is not a trivial task. To get started with React and modern JavaScript a tool like create-react-app can be used:
https://github.com/facebookincubator/create-react-app
It will set up a React template so you can get started immediately with your code. You can eject out of the create-react-app environment with yarn eject (it is a one-way exit, though). This will replace the initial development environment with webpack- and babel-configurations that you can continue to extend upon.
Styling and components
In the example app, I have added material-ui for styling and some ready-to-use UI-components like a toogle switches and sliders.
The Philips Hue api
The Philips Hue bridge has a RESTful api that can be accessed with http-requests:
https://developers.meethue.com/documentation/getting-started
If you have the IP-address of the Hue bridge, you can start the CLIP API Debugger in a browser on the same network:
http://<IP_ADDRESS_OF_HUE_BRIDGE>/debug/clip.html
To get started, you need to send a Post request to the Hue bridge for generating a new username token:
Press the button on the Hue bridge, and then press Post with a message body as above (replace the devicetype property with anything you like). This will return a new username token for you:
This token can then be used for accessing the API of your Hue bridge. For example, to get information on all lights in your system, send a GET request to a URL that includes the username after api/:
http://<IP_ADDRES_OF_HUE_BRIDGE>/api/5Bg8MEr2C3GKldw4SYsAEp8BLiJ6GK41dZnUABA5/lights
In the CLIP API Debugger:
The GET request returns a JSON object with a dictionary (key-value pairs) for all your lights.
Similarly, you can get a list of all the Hue groups in your system with:
http://<IP_ADDRES_OF_HUE_BRIDGE>/api/5Bg8MEr2C3GKldw4SYsAEp8BLiJ6GK41dZnUABA5/groups
To change the state or the brightness of a light, you send a PUT request with a body. For example, to turn on the light with id “6” and set the brightness to 150:
My example app, as described below, uses a GET request to get the on/of states and brightness for all the lights in a Hue system. When you use the toggle switches and sliders in the GUI, it will send PUT requests for updating the state of the lights.
The example app
If you have a Hue system, you can try out my React app from GitHub. The app fetches all lights from the Hue bridge and let’s you toggle the lights on/off and change the brightness of each light via a slider. The app polls for state changes from the bridge every 5 seconds, so if you use another app to for changing the lights states, it will be reflected in this web app’s GUI within 5 seconds.
Building and running the application
Download and install git if you don’t have it on your system already.
Download and install yarn if you don’t have it on your system already.
Get the code for the application:
git clone https://github.com/LarsBergqvist/philips_hue_react_app.git
Check (via your router) what IP-address your Philips Hue bridge has been assigned on your local network, e.g. 192.168.1.232.
Create a new user-token with the CLIP API Debugger on the bridge, see https://developers.meethue.com/documentation/getting-started
Modify philips_hue_react_app/src/config.json with the IP-address and the new user-token. For example:
{ "apiUrl": "http://192.168.1.232", "username":"5Bg8MEr2C3GKldw4SYsAEp8BLiJ6GK41dZnUABA5" }
Install the dependencies, build and start the app:
cd philips_hue_react_app yarn install yarn start
The application should start on localhost with port 3000:
The code
The main part of the application is a React component called LightsView. It fetches Philips Hue state data before the component is mounted (and also every 5 seconds via a timer).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from 'react'; | |
import LightItem from './LightItem'; | |
import Config from './config'; | |
class LightsView extends Component { | |
constructor(props) { | |
super(props); | |
this.requestFailed = false; | |
this.data = null; | |
this.onToggleLight = this.onToggleLight.bind(this); | |
this.fetchData = this.fetchData.bind(this); | |
this.onBrightnessChanged = this.onBrightnessChanged.bind(this); | |
setInterval(this.fetchData,5000); | |
} | |
componentWillMount() { | |
this.fetchData(); | |
} | |
getUrlWithUsername() { | |
return Config.apiUrl + '/api/' + Config.username + '/lights'; | |
} | |
fetchData() { | |
let url = this.getUrlWithUsername(); | |
fetch(url) | |
.then(response => { | |
if (!response.ok) { | |
throw Error('Network request failed'); | |
} | |
return response; | |
}) | |
.then(d => d.json()) | |
.then(d => { | |
this.data = d; | |
this.requestFailed = false; | |
this.setState({newData:new Date()}); | |
}, () => { | |
this.requestFailed = true; | |
this.setState({newData:new Date()}); | |
}) | |
} | |
changeState(id, bodyData) { | |
let url = this.getUrlWithUsername() + '/' + id + '/state'; | |
fetch(url, { method: 'PUT', body: bodyData }) | |
.then(response => { | |
if (!response.ok) { | |
throw Error('Network request failed'); | |
} | |
return response; | |
}) | |
.then(d => d.json()) | |
.then(d => { | |
this.requestFailed = false; | |
this.fetchData(); | |
}, () => { | |
this.requestFailed = true; | |
}) | |
} | |
onToggleLight(id, isOn) { | |
let bodyData = '{"on":' + !isOn + '}'; | |
this.changeState(id, bodyData); | |
} | |
onBrightnessChanged(id, newValue) { | |
let bodyData = '{"bri":' + newValue + '}'; | |
this.changeState(id, bodyData); | |
} | |
render() { | |
if (this.requestFailed) { | |
let url = this.getUrlWithUsername(); | |
return <p className='warning'>Could not fetch from {url}</p> | |
} | |
if (!this.data) { | |
return <p>Loading…</p>; | |
} | |
if (this.data[0] !== undefined) { | |
return <p className='warning'>{this.data[0].error.description}</p>; | |
} | |
let data = this.data; | |
let lightItems = []; | |
let toggleHandler = this.onToggleLight; | |
let brightnessHandler = this.onBrightnessChanged; | |
Object.keys(data).forEach(function(id,index) { | |
let item = data[id]; | |
let light = <LightItem key={id} id={id} name={data[id].name} | |
isOn={item.state.on} bri={item.state.bri} | |
reachable={item.state.reachable} | |
onToggleLight={toggleHandler} | |
onBrightnessChanged={brightnessHandler}/> | |
lightItems.push(light); | |
}); | |
return ( | |
<div align='center' style={{maxWidth:950,margin: '20px auto 0'}}> | |
{lightItems} | |
</div> | |
); | |
} | |
} | |
export default LightsView; |
The LightsView component renders an array of LightItem components (one item for each Philips Hue light found). Callback methods defined in LightsView are sent as props arguments for each LightItem. When a light is toggled on/off in the GUI or when a brightness slider changes its value, this callback is called on the LightsView component and the LightsView makes a PUT request with new states for the corresponding Hue light to the Hue bridge.
A LightItem component only does rendering and routes callbacks to its parent component (via props-properties sent to the LightItem from its parent), so it can be written as one single render method:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import Toggle from 'material-ui/Toggle'; | |
import Slider from 'material-ui/Slider'; | |
const LightItem = (props) => ( | |
<div className='items'> | |
<div className='item toggle'> | |
<Toggle | |
toggled={props.isOn} | |
label={props.name} | |
onToggle={() => props.onToggleLight(props.id,props.isOn)} | |
disabled={!props.reachable} | |
/> | |
{props.reachable ? '' : <div className='warning'>not reachable</div>} | |
</div> | |
{props.reachable ? | |
<div className='item slider'> | |
<Slider | |
min={0} | |
max={255} | |
step={1} | |
value={props.bri} | |
onChange={(event,newValue) => props.onBrightnessChanged(props.id,newValue)} | |
/> | |
</div> : '' } | |
</div> | |
); | |
export default LightItem; |
If a Hue light returns reachable=false as state, it means that the light can not be contacted by the Hue bridge (this happens if the light bulb has been physically turned off e.g.). In that case, the LightItem renders a text ‘not reachable’.
Going further
Feel free to build upon this initial sketch. Perhaps adding a view for displaying and controlling Hue groups? If you have Philips Hue lights with color (which I don’t), add a GUI for modifying the color of the lights.
https://github.com/LarsBergqvist/philips_hue_react_app