Walle - Home Assistant
About the project
Home automation system with built-in Alexa assistant and wireless controllers for light and central heating? It sounds great, doesn't it?
Project info
Difficulty: Difficult
Platforms: Amazon Alexa, Arduino, Raspberry Pi, SparkFun, XBee
Estimated time: 1 week
License: GNU General Public License, version 3 or later (GPL3+)
Items used in this project
Hardware components
View all
Software apps and online services
Story
What is Walle - Home Assistant?
During last weeks we were working very hard creating a device, which helps people in a better day organisation. The device shows date with time, our events from a calendar, weather forecast and also breaking news. Everything you need to know at first sight.
An assistant is connected with a Wi-Fi wireless home network, that’s why all of data is always up-to-date. The only thing you need to do is plugging in the charger to the power source.
Walle is not only a main screen, on which we see preview of applications. When you click on one of the section you are moved to a fullscreen view, which contains extended information. You can for example get access to clear and beautiful full-size calendar - a smart calendar with short summary of upcoming events in each day.
Our device also has got built-in Amazon Alexa assistant, with whom you can have a normal chat like with a real person, but not only! We have created two Walle Controllers - the first for the light and the second one for the central heating system. They are standalone devices with wireless communication, that’s why Walle is able to easily control every kind of home automation.
So what is really Walle? It’s a complete home automation system!
Our latest achievement
Walle has won Best Alexa Voice Service Integration award in The Alexa and Arduino Smart Home Challenge competition organised by Hackster.io and Amazon. We are so proud, because our project has been chosen out of 129 other entries and nearly 1500 participants. Our dream to be an award winner of the international competition has become reality, but we don't rest on our laurels and we're still developing the Walle!
How it looks?
Main view
Weather view
Calendar view
How it works?
Alexa - Central Heating On
Manual Central Heating Off
Alexa - Central Heating System - Target Temperature
Alexa - The Light On
Manual - The Light On & Off
How can I start and build own Walle?
We think the most necessary things you must have are patience and a great deal of free time. But if you are reading this, I’m assume that you’re ready. Well, let's get started!
Getting started with Raspberry Pi
Firstly, go to Raspberry Pi Official Website and download the latest release of Raspbian Desktop. After successful download, unzip file and now you must flash it to the microSD card (8GB of memory is required). I recommend you using Etcher - a graphical SD card writing tool, which works on macOS, Linux and Windows. It's super easy.
After some time you should have installed a clean Raspbian. Attach it to the Raspberry, connect with keyboard, mouse and monitor and plug in the charger to the power source.
If you get your system running up, connect to the wireless Wi-Fi network or use Ethernet port with network cable. Now click the top right Raspberry icon, go to the Raspberry Pi Configuration, open 'Interfaces' tab and enable SSH connection.
Connect to Raspberry using magic
Open Terminal or in case you are using Windows use some free SSH client (for example PuTTY) and connect with your Raspberry by typing ssh pi@ip_address where ip_address is Raspberry's ip address in local network. Then you will be asked for a password. The default for Raspbian is
raspberry.It's time to change your password, because of course you don't want to have some intruder messing up your future work. Type sudo passwd pi
command, provide default password and then enter your new password.Connect your LCD monitor and configure Raspberry
I'm using 15,4" LCD screen with resolution 1280x800, which is taken from laptop. That's why I need to connect a few things like inverter or backlight and then finally Raspberry with controller board using HDMI cable. Now we're going to adjust resolution and rotate the screen by editing configuration file inside Raspberry.
Type sudo nano /boot/config.txt command in SSH client. This will open a standard text editor, in which you have to move using your keyboard. Inside the file, find these two lines: hdmi_group and hdmi_mode. Uncomment them and set as following:
hdmi_group=2
hdmi_mode=27
If you have other resolution than me, you can find it in one of these two tables (for CEA or DMT group) on Raspberry Pi Official Website and edit as needed.
In addition you must add a new line in the beginning (it actually doesn't matter where) of the file to rotate the screen by 270 degrees. And the line is display_rotate=3.
To save and close the text editor press Ctrl+X, Y, Enter.
Unfortunately, it was only one and the last pleasent part of creating this home automation system.
Reboot your Raspberry by typing sudo reboot to see the changes and get ready for the much harder work. I won't describe it in every detail like before, but at the end of this page you will find contact information to me. Feel free to ask every single question! I will be delighted to see if someone is interested in my work!
Connect your touch screen panel
I'm using 15,4" 4-wire resistive touch screen panel made by eGalax with USB controller. Connect it to the Raspberry and using SSH update your Raspbian and install some neccessary packages:
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install xserver-xorg-input-evdev libx11-dev libxext-dev libxi-dev x11proto-input-dev
At this moment the touch screen panel isn't probably working. Let's be sure if we're using eGalax product and kernel has got support for it. By typing lsmod | grep touch command you should get something like usbtouchscreen in response.
You can also check detailed information about connected devices using cat /proc/bus/input/devices command. You should see there your eGalax Inc. USB TouchScreen.
Now restart Raspberry and your touch screen panel should work correctly. If not, just let me know.
Calibrate your touch screen panel
In this part you only need to type all of these commands to download the xinput_calibrator:
wget http://github.com/downloads/tias/xinput_calibrator/xinput_calibrator-0.7.5.tar.gz
./configure
make
sudo
If you're typing your commands using SSH client you also need to access Raspberry's local display from outside the local sessions. So type export DISPLAY=:0 command and then you can run your calibrator by typing xinput_calibrator. Just follow the instructions and calibrate your touch screen (use stylus for better precision).
Install Node.JS on Raspberry
Type this two commands to get the latest version (at the moment is 9.6.1) of Node:
curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
sudo apt-get install nodejs
You can check you current version by typing node -v command.
Build an Electron with React application
To build a basic application, type these commands:
cd ~
create-react-app
cd ./walle
npm
To see if everything is working type npm start to run the development server and then npm run electron to open standalone application.
Replace some files
To get my application working you need to replace some files. They aren't very complicated, so there is no need for talking them out.
main.js (create this one in a parent folder)
const {app, BrowserWindow} = require('electron')
let win = null;
function createWindow() {
win = new BrowserWindow({width: 800, height: 1280});
win.loadURL('http://localhost:3000');
win.webContents.openDevTools()
win.on('closed', function () {
win = null;
});
}
app.on('ready', function () {
createWindow();
});
app.on('activate', () => {
if (win === null)
createWindow();
});
app.on('window-all-closed', function () {
if (process.platform != 'darwin')
app.quit();
});
./public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<title>React App</title>
<link rel="stylesheet" href="./css/MainScreen.css">
<link rel="stylesheet" href="./css/weather.css">
<link rel="stylesheet" href="./css/calendar.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
html {
background: url("./css/bg.jpg");
background-position: center;
cursor: default;
font-family: 'Oxygen', sans-serif;
}
body {
height: 100vh;
margin: 0;
background: rgba(10,10,10,0.4);
color: white;
}
</style>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>
./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import MainScreen from './main/MainScreen';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(<MainScreen />, document.getElementById('root'));
registerServiceWorker();
React: add main view and other components
Main view is a screen, which is visible to users at first. It holds every single component.
./src/main/MainScreen.js
import React from 'react';
import DateTime from './DateTime';
import Events from './Events';
import Weather from './Weather';
import News from './News';
import Home from './Home';
class MainScreen extends React.Component {
render() {
return(
<div>
<DateTime />
<Events />
<Weather />
<News />
<Home />
</div>
)
}
}
export default MainScreen
./src/main/DateTime.js
import React from 'react';
import backDateTime from './back/backDateTime';
class DateTime extends React.Component {
constructor(props) {
super(props);
this.dateTime = new backDateTime();
this.state = {
time: this.dateTime.getTime(),
date: this.dateTime.getDate()
};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
this.dateTime.destroy();
}
tick() {
this.dateTime.destroy();
this.dateTime = new backDateTime();
this.setState({
time: this.dateTime.getTime(),
date: this.dateTime.getDate()
});
}
render() {
return (
<div className='dateContainer'>
<div className='clock'>
<div className='clock1'>{this.state.time.hours}:{this.state.time.minutes}</div>
<div className='clock2'>
<div>{this.state.time.seconds}</div>
<div>{this.state.time.divide}</div>
</div>
</div>
<div className='date'>
<div className='dayOfWeek'>{this.state.date.weekday}</div>
<div className='month'>{this.state.date.monthname} {this.state.date.day} <sup>{this.state.date.ordinal}</sup></div>
</div>
</div>
)
}
}
export default DateTime
./src/main/Events.js
import React from 'react';
import ReactDOM from 'react-dom';
import Calendar from '../fullscreen/calendar';
import backCalendar from './back/backCalendar';
class Events extends React.Component {
constructor(props) {
super(props);
this.backCalendar = new backCalendar();
this.state = {
upcoming: []
};
this.backCalendar.getUpcomingEvents().then(res => {
this.setState({
upcoming: (res.quantity) ? res.events : []
});
});
this.goToCalendarComponent = this.goToCalendarComponent.bind(this);
}
goToCalendarComponent() {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
ReactDOM.render(<Calendar />, document.getElementById('root'));
}
getSummaryOfEvent(event) {
if (this.state.upcoming !== undefined)
if (this.state.upcoming[event] !== undefined)
return this.state.upcoming[event].summary;
else
return '';
else
return '';
}
getStartOfEvent(event) {
if (this.state.upcoming !== undefined)
if (this.state.upcoming[event] !== undefined)
if (this.state.upcoming[event].start !== undefined)
if (this.state.upcoming[event].start.time !== undefined)
return this.state.upcoming[event].start.time.hours + ':' + this.state.upcoming[event].start.time.minutes;
else
return '';
else
return '';
else
return '';
else
return '';
}
getEndOfEvent(event) {
if (this.state.upcoming !== undefined)
if (this.state.upcoming[event] !== undefined)
if (this.state.upcoming[event].end !== undefined)
if (this.state.upcoming[event].end.time !== undefined)
return this.state.upcoming[event].end.time.hours + ':' + this.state.upcoming[event].end.time.minutes;
else
return '';
else
return '';
else
return '';
else
return '';
}
render() {
return(
<div className='events' onClick={this.goToCalendarComponent}>
<div>Upcoming events</div>
<div className='eventsList'>
{this.state.upcoming.map((e, i) =>
<div key={i}>{this.getStartOfEvent(i)} - {this.getEndOfEvent(i)} {this.getSummaryOfEvent(i)}</div>
)}
</div>
</div>
)
}
}
export default Events
./src/main/Weather.js
import React from 'react';
import ReactDOM from 'react-dom';
import WeatherFS from '../fullscreen/weather';
import backWeather from './back/backWeather';
class Weather extends React.Component {
constructor(props) {
super(props);
this.state = {
today: {img: './icons/weather/mostly_cloudy.svg'},
day1: {condition_img: './icons/weather/mostly_cloudy.svg'},
day2: {condition_img: './icons/weather/mostly_cloudy.svg'},
day3: {condition_img: './icons/weather/mostly_cloudy.svg'}
}
this.weather = new backWeather();
this.weather.getToday().then(res => {
this.setState({
today: res
});
});
this.weather.getForecast().then(res => {
this.setState({
day1: res[1],
day2: res[2],
day3: res[3]
});
});
this.goToWeatherComponent = this.goToWeatherComponent.bind(this);
}
goToWeatherComponent() {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
ReactDOM.render(<WeatherFS />, document.getElementById('root'));
}
componentDidMount() {
this.timerWeather = setInterval(() => {
this.weather = new backWeather();
this.weather.getToday().then(res => {
this.setState({
today: res
});
});
this.weather.getForecast().then(res => {
this.setState({
day1: res[1],
day2: res[2],
day3: res[3]
});
});
}, 1000*5)
}
componentWillUnmount() {
clearInterval(this.timerWeather);
}
getObservationDate() {
if (this.state.today !== undefined)
if (this.state.today.observation !== undefined)
return this.state.today.observation.date.full;
else
return '';
else
return '';
}
getObservationTime() {
if (this.state.today !== undefined)
if (this.state.today.observation !== undefined)
return this.state.today.observation.time.hours + ':' + this.state.today.observation.time.minutes;
else
return '';
else
return '';
}
render() {
return(
<div className='weather' onClick={this.goToWeatherComponent}>
<div className='today'>
<div className='left'>
<img className='icon' src={require(`${this.state.today.img}`)} alt='icon' />
<div className='temp'>{this.state.today.temp} {this.state.today.temp_unit}</div>
</div>
<div className='right'>
<div className='condition'>{this.state.today.condition}</div>
<div className='feels'>Feels like {this.state.today.feelslike} {this.state.today.temp_unit}</div>
<div className='wind'>Wind {this.state.today.wind} {this.state.today.wind_unit}</div>
</div>
</div>
<div className='fore'>
<div className='day1'>
<div className='weekday'>{this.state.day1.weekday}</div>
<img className='icon' src={require(`${this.state.day1.condition_img}`)} alt='icon'/>
<div className='temp'>{this.state.day1.temp_high} {this.state.day1.temp_unit}</div>
</div>
<div className='day2'>
<div className='weekday'>{this.state.day2.weekday}</div>
<img className='icon' src={require(`${this.state.day2.condition_img}`)} alt='icon'/>
<div className='temp'>{this.state.day2.temp_high} {this.state.day2.temp_unit}</div>
</div>
<div className='day3'>
<div className='weekday'>{this.state.day3.weekday}</div>
<img className='icon' src={require(`${this.state.day3.condition_img}`)} alt='icon'/>
<div className='temp'>{this.state.day3.temp_high} {this.state.day3.temp_unit}</div>
</div>
</div>
<div className='location'>
<i className='material-icons'>location_on</i><span>{this.state.today.loc_city}, {this.state.today.loc_country}</span>
</div>
<div className='update'>
Last updated on {this.getObservationDate()} at {this.getObservationTime()}
</div>
</div>
)
}
}
export default Weather
./src/main/News.js
import React from 'react';
import backNews from './back/backNews';
class News extends React.Component {
constructor(props) {
super(props);
this.state = {
news: {}
};
this.news = new backNews();
this.news.getLatest().then(res => {
this.setState({
news: res
});
});
}
componentDidMount() {
this.timerNews = setInterval(() => {
this.news = new backNews();
this.news.getLatest().then(res => {
this.setState({
news: res
});
});
}, 1000*5)
}
componentWillUnmount() {
clearInterval(this.timerNews);
}
render() {
return(
<div className='news'>
<div className='source'>{this.state.news.source} latest news</div>
<div className='title'>{this.state.news.title}</div>
<div className='description'>{this.state.news.desc}</div>
</div>
)
}
}
export default News
./src/main/Home.js
import React from 'react';
import backHome from './back/backHome';
class Home extends React.Component {
constructor() {
super();
this.backHome = new backHome();
this.state = {
light: 'false',
centralHeating: 'false',
currentTemp: 0,
targetTemp: 0
};
this.switchLight = this.switchLight.bind(this);
this.switchCentralHeating = this.switchCentralHeating.bind(this);
}
componentDidMount() {
this.timerLightStatus = setInterval(
() => this.checkLight(),
1000
);
this.timerCentralHeatingStatus = setInterval(
() => this.checkCentralHeating(),
1000
);
this.timerCurrentTemp = setInterval(
() => this.getCurrentTemp(),
1000
);
this.timerTargetTemp = setInterval(
() => this.getTargetTemp(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerLightStatus);
clearInterval(this.timerCentralHeatingStatus);
clearInterval(this.timerCurrentTemp);
clearInterval(this.timerTargetTemp);
}
checkLight() {
this.backHome.checkLight().then(res => {
this.setState({
light: res
});
});
}
checkCentralHeating() {
this.backHome.checkCentralHeating().then(res => {
this.setState({
centralHeating: res
});
});
}
getCurrentTemp() {
this.backHome.readCurrentTemp().then(res => {
this.setState({
currentTemp: res
});
});
}
getTargetTemp() {
this.backHome.readTargetTemp().then(res => {
this.setState({
targetTemp: res
});
});
}
switchLight() {
if (this.state.light === 'false')
this.backHome.lightOn().then(res => {
if (res === true)
this.setState({
light: 'true'
});
});
else if (this.state.light === 'true')
this.backHome.lightOff().then(res => {
if (res === true)
this.setState({
light: 'false'
});
});
}
switchCentralHeating() {
if (this.state.centralHeating === 'false')
this.backHome.centralHeatingOn().then(res => {
if (res === true)
this.setState({
centralHeating: 'true'
});
});
else if (this.state.centralHeating === 'true')
this.backHome.centralHeatingOff().then(res => {
if (res === true)
this.setState({
centralHeating: 'false'
});
});
}
render() {
return(
<div className='home'>
<div>Home automation</div>
<div className='lights' bg={this.state.light} onClick={this.switchLight}><div><i className="material-icons">lightbulb_outline</i></div><div>Light</div></div>
<div className='heating'>
<div className='status'>
Automatic control
<div className='temps'>
<div className='current'>Current temp: {this.state.currentTemp} °C</div>
<div className='target'>Target temp: {this.state.targetTemp} °C</div>
</div>
</div>
<div className='heatingBtn' bg={this.state.centralHeating} onClick={this.switchCentralHeating}>
<i className="material-icons">whatshot</i>
<div>Central Heating</div>
</div>
</div>
</div>
)
}
}
export default Home
React: fullscreen components
More and more code...
./src/fullscreen/calendar.js
import React from 'react';
import ReactDOM from 'react-dom';
import MainScreen from '../main/MainScreen';
import backDateTime from '../main/back/backDateTime';
import backCalendar from '../main/back/backCalendar';
function Card(props) {
return (
<div className='card'>
<div today={props.today}><span>{props.day}</span></div>
<div className='eventsCard'>
<div>{(props.event1) ? `• ${props.event1}` : ''}</div>
<div>{(props.event2) ? `• ${props.event2}` : ''}</div>
<div>{(props.event3) ? `• ${props.event3}` : ''}</div>
</div>
</div>
)
}
class Calendar extends React.Component {
constructor(props) {
super(props);
this.dateTime = new backDateTime();
this.backCalendar = new backCalendar();
this.state = {
time: this.dateTime.getTime(),
year: this.dateTime.getDate().year,
month: this.dateTime.getDate().month,
day: this.dateTime.getDate().day,
events: []
};
this.backCalendar.getMonthEvents(this.state.year, this.state.month+1).then(res => {
this.setState({
events: res
});
});
this.clickForward = this.clickForward.bind(this);
this.clickBackward = this.clickBackward.bind(this);
this.backToMainScreen = this.backToMainScreen.bind(this);
}
backToMainScreen() {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
ReactDOM.render(<MainScreen />, document.getElementById('root'));
}
componentDidMount() {
this.timerDateTime = setInterval(
() => this.tick(),
1000
);
}
tick() {
this.dateTime.destroy();
this.dateTime = new backDateTime();
this.setState({
time: this.dateTime.getTime(),
day: this.dateTime.getDate().day
});
}
componentWillUnmount() {
this.dateTime.destroy();
clearInterval(this.timerDateTime);
}
clickForward() {
this.setState(prevState => ({
year: (prevState.month < 11) ? prevState.year : prevState.year+1,
month: (prevState.month < 11) ? prevState.month+1 : 0,
events: []
}));
setTimeout(() => {
this.backCalendar.getMonthEvents(this.state.year, this.state.month+1).then(res => {
this.setState({
events: res
});
});
}, 1000);
}
clickBackward() {
this.setState(prevState => ({
year: (prevState.month > 0) ? prevState.year : prevState.year-1,
month: (prevState.month > 0) ? prevState.month-1 : 11,
events: []
}));
setTimeout(() => {
this.backCalendar.getMonthEvents(this.state.year, this.state.month+1).then(res => {
this.setState({
events: res
});
});
}, 1000);
}
days() {
let tab = [];
for(let i=1; i<=this.monthLength(this.state.month, this.state.year); i++)
tab[i] = i;
return tab;
}
monthLength(m, y) {
return new Date(y, m+1, 0).getDate();
}
monthGap(m, y) {
let gap = new Date(y, m, 1).getDay()-1;
return (gap >= 0) ? gap : 6;
}
monthName(month) {
let months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
return months[month];
}
getSpecificEvent(day, event) {
if (this.state.events[day-1] !== undefined)
if (this.state.events[day-1].events !== undefined)
if (this.state.events[day-1].events[event-1] !== undefined)
return this.state.events[day-1].events[event-1].summary;
else
return '';
else
return '';
else
return '';
}
render() {
return (
<div className='calendar'>
<div className='menu' onClick={this.backToMainScreen}><i className='material-icons'>arrow_back</i><span>Back</span></div>
<div className='head'>
<div className='year'>{this.state.year}</div>
<div className='day'>{this.state.day}</div>
<div className='hour'>{this.state.time.full}</div>
</div>
<div className="actual">
<button type='button' onClick={this.clickBackward}>Backward</button>
<div>{this.monthName(this.state.month)}</div>
<button type='button' onClick={this.clickForward}>Forward</button>
</div>
<div className='calendarGrid'>
<div className='daysOfWeek'>
<div>Mon</div>
<div>Tue</div>
<div>Wed</div>
<div>Thu</div>
<div>Fri</div>
<div>Sat</div>
<div>Sun</div>
</div>
{[...Array(this.monthGap(this.state.month,this.state.year))].map((e,i) => <Card day=' ' key={e} />)}
{this.days().map((v,i) => <Card day={v} today={(v === this.state.day && new Date().getMonth() === this.state.month && new Date().getFullYear() === this.state.year) ? 'y' : ''} event1={this.getSpecificEvent(v, 1)} event2={this.getSpecificEvent(v, 2)} event3={this.getSpecificEvent(v, 3)} key={`${this.state.month}_${v}`} /> )}
</div>
</div>
)
}
}
export default Calendar
./src/fullscreen/weather.js
import React from 'react';
import ReactDOM from 'react-dom';
import MainScreen from '../main/MainScreen';
import backWeather from '../main/back/backWeather';
import backDateTime from '../main/back/backDateTime';
class WeatherFS extends React.Component {
constructor(props) {
super(props);
this.weather = new backWeather();
this.dateTime = new backDateTime();
this.state = {
today: {img: './icons/weather/mostly_cloudy.svg'},
forecast: [],
hourly: [],
date: this.dateTime.getDate(),
time: this.dateTime.getTime()
}
this.weather.getToday().then(res => {
this.setState({
today: res
});
});
this.weather.getForecast().then(res => {
this.setState({
forecast: res
});
});
this.weather.getHourly().then(res => {
this.setState({
hourly: res
});
});
this.backToMainScreen = this.backToMainScreen.bind(this);
}
backToMainScreen() {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
ReactDOM.render(<MainScreen />, document.getElementById('root'));
}
componentDidMount() {
this.timerDateTime = setInterval(() => {
this.dateTime.destroy();
this.dateTime = new backDateTime();
this.setState({
date: this.dateTime.getDate(),
time: this.dateTime.getTime()
});
}, 1000);
this.timerWeather = setInterval(() => {
this.weather = new backWeather();
this.weather.getToday().then(res => {
this.setState({
today: res
});
});
this.weather.getForecast().then(res => {
this.setState({
forecast: res
});
});
this.weather.getHourly().then(res => {
this.setState({
hourly: res
});
});
}, 1000*5)
}
componentWillUnmount() {
clearInterval(this.timerDateTime);
clearInterval(this.timerWeather);
this.dateTime.destroy();
}
render() {
return (
<div className='weatherFS' onClick={this.backToMainScreen}>
<div className='menu'><i className='material-icons'>arrow_back</i><span>Back</span></div>
<div className='today'>
<div className='city'>{this.state.today.loc_city}</div>
<div className='todayDate'>{this.state.date.weekday},{' '}{this.state.date.full} {this.state.time.full}</div>
<div className='todayCond'><img className='icon' src={require(`${this.state.today.img}`)} alt='icon' /><span className='temp'>{this.state.today.temp} {this.state.today.temp_unit}</span></div>
<div className='wCondition'>{this.state.today.condition}</div>
<div className='feel'>Feels like {this.state.today.feelslike} {this.state.today.temp_unit}</div>
</div>
<div className='hourly'>
{this.state.hourly.map((e,i)=>{
return (
<div className={`hourly${i}`} key={e.time.slice(0,2)}>
<div>{e.time}</div>
<img className='icon' src={require(`${e.condition_img}`)} alt='icon' />
<div>{e.temp}{e.temp_unit}</div>
</div>
)
})}
</div>
<div className='extended'>
<div>
<span>Humidity</span>
<div className='value'>{this.state.today.humidity}</div>
</div>
<div>
<span>Wind</span>
<div className='value'>{this.state.today.wind} {this.state.today.wind_unit}</div>
</div>
<div>
<span>UV Index</span>
<div className='value'>0</div>
</div>
<div>
<span>Pressure</span>
<div className='value'>{this.state.today.pressure} {this.state.today.pressure_unit}</div>
</div>
</div>
<div className='forecast'>
{this.state.forecast.map((e,i)=>{
return (
<div className={`forecast${i}`} key={e.weekday.slice(0,3)}>
<div className='weekday'>{e.weekday}</div>
<img className='icon' src={require(`${e.condition_img}`)} alt='icon' />
<div className='humid'>{e.humidity_ave}</div>
<div className='temp_high'><i className='material-icons'>keyboard_arrow_up</i>{e.temp_high} {this.state.today.temp_unit}</div>
<div className='temp_low'><i className='material-icons'>keyboard_arrow_down</i>{e.temp_low} {this.state.today.temp_unit}</div>
</div>
)
})}
</div>
</div>
)
}
}
export default WeatherFS
React: add some icons
You can download a big pack of icons from Flaticon Website. Put them in the correct path and you will have a nice UI.
React: add some CSS stylesheets
./public/css/MainScreen.css
@import url('https://fonts.googleapis.com/css?family=Overpass+Mono|Oxygen');
/* DATETIME */
.dateContainer {
margin: 0 2vh;
padding: 5vw;
height: 5vh;
}
.dateContainer .clock {
float: left;
width: 50%;
}
.dateContainer .date {
float: left;
width: 50%;
}
.clock1 {
width: 50%;
float: left;
font-family: 'Overpass Mono', monospace;
font-size: 45pt;
}
.clock2 {
width: 50%;
height: 65px;
float: left;
font-family: 'Overpass Mono', monospace;
font-size: 16pt;
line-height: 30px;
position: relative; top: 15px; left: 30px;
}
.date {
text-align: right;
}
.date .dayOfWeek {
font-family: 'Oxygen', sans-serif;
font-size: 28pt;
}
.date .month {
font-family: 'Oxygen', sans-serif;
font-size: 21pt;
}
/* EVENTS */
.events * {
font-family: 'Oxygen', sans-serif;
font-size: 12pt;
}
.events {
margin: 3vh 2vh;
padding: 2vw 5vw;
}
.events > div:first-child {
font-size: 20pt;
margin-bottom: 20px;
}
.eventsList > div {
margin-top: 5px;
margin-left: 40px;
font-size: 17px;
}
/* WEATHER */
.weather * {
font-family: 'Oxygen', sans-serif;
font-size: 12pt;
}
.weather {
margin: 1vh 2vh;
padding: 2vw 5vw;
height: 12vh;
}
.weather .today, .fore, .day1, .day2, .day3 {
display: inline-block;
}
.weather .today {
padding-right: 3vw;
width: 25vw;
margin-left: 15px;
}
.weather .today .left {
width: 49%;
float: left;
text-align: center;
}
.weather .today .right {
width: 51%;
float: right;
margin-top: 30px;
position: relative; left: 30px;
text-align: center;
}
.right div {
margin-top: 10px;
}
.fore {
border-left: 1px solid black;
padding-left: 3vw;
margin-left: 30px;
}
.day1, .day2, .day3 {
text-align: center;
width: 11.49vw;
padding: 2vw;
}
.day1 > img, .day2 > img, .day3 > img {
width: 60%;
}
.feels {
font-size: 11pt;
}
.location {
width: 50%;
float: left;
margin-top: 30px;
}
.location > span {
position: relative; top: -5px; left: 10px;
}
.update {
width: 50%;
float: left;
text-align: right;
margin-top: 30px;
}
/* NEWS */
.news * {
font-family: 'Oxygen', sans-serif;
font-size: 12pt;
}
.news {
margin: 2vh;
margin-top: 110px;
padding: 2vw 5vw 2vw 5vw;
}
.news .source {
font-size: 20pt;
}
.news .title {
margin-top: 40px;
font-weight: bolder;
font-size: 13pt;
}
.news .description {
margin-top: 25px;
}
/* HOME */
.home * {
font-family: 'Oxygen', sans-serif;
font-size: 12pt;
}
.home {
margin: 6vh 2vh;
padding: 2vw 5vw 2vw 5vw;
}
.home > div:first-child {
font-size: 20pt;
margin-bottom: 2vh;
}
.home .lights i {
font-size: 50pt;
}
.home .lights div{
font-size: 16pt;
}
.home .lights {
display: inline-block;
width: 18vw;
border: 1px solid white;
padding: 5px;
text-align: center;
border-radius: 15px;
}
.home .heating {
display: inline-block;
}
.home .heating .status {
display: inline-block;
width: 25vw;
margin-left: 120px;
text-align: center;
font-size: 14pt;
line-height: 35px;
}
.home .heating .temps {
font-size: 14pt;
margin-top: 25px;
line-height: 28px;
}
.home .heatingBtn {
display: inline-block;
border: 1px solid white;
border-radius: 15px;
width: 21vw;
text-align: center;
padding: 10px;
}
.home .heatingBtn i {
font-size: 50pt;
}
.home .heatingBtn div{
font-size: 16pt;
}
.lights[bg='true'], .heatingBtn[bg='true'] {
background-color: rgba(255, 255, 255, 0.4);
}
./public/css/calendar.css
.menu {
position: relative; top: 15px; left: 15px;
}
.menu > span {
position: relative; top: -5px; left: 5px;
}
.calendar .head {
margin: 0 2vw;
text-align: center;
}
.calendar .actual {
margin: 6vw 2vw;
text-align: center;
}
.actual button:nth-child(1) {
float: left;
margin-left: 50px;
margin-top: 20px;
}
.actual button:nth-child(3) {
float: right;
margin-right: 50px;
margin-top: 20px;
}
.actual div {
display: inline-block;
font-size: 50px;
}
.calendarGrid {
margin: 0 2vw;
}
.year, .day, .hour {
width: 33%;
display: inline-block;
}
.year, .hour {
font-size: 50px;
}
.day {
font-size: 90px;
}
.daysOfWeek > div {
width: 13.4vw;
margin-bottom: 30px;
float: left;
text-align: center;
font-size: 30px;
}
.card {
width: 13.4vw;
height: 140px;
float: left;
text-align: center;
}
.card > div:nth-child(1) {
width: 60px;
height: 58px;
border-radius: 50%;
margin: 0 auto;
}
.card > div:nth-child(1)[today='y'] {
background-color: red;
}
.card > div > span {
font-size: 36px;
position: relative; top: 5px;
}
.eventsCard > div {
font-size: 14px;
margin-top: 5px;
}
.eventsCard > div:nth-child(1) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
./public/css/weather.css
@import url('https://fonts.googleapis.com/css?family=Oxygen');
.menu > span {
position: relative; top: -5px;
}
.weatherFS * {
font-family: 'Oxygen', sans-serif;
font-size: 14pt;
color: white;
}
.weatherFS .today {
height: 15vh;
text-align: center;
margin: 2vw;
padding: 2vw 5vw;
}
.weatherFS .today .city {
font-weight: bold;
font-size: 16pt;
}
.weatherFS .today .todayDate, .weatherFS .today .feel {
margin-top: 10px;
}
.weatherFS .todayCond .icon {
width: 20%;
}
.weatherFS .todayCond .temp {
font-size: 48pt;
font-weight: lighter;
position: relative; top: -40px;
margin-left: 20px;
}
.wCondition {
text-align: center;
font-weight: bold;
font-size: 16pt;
}
.hourly {
margin: 3vh 2vw;
padding: 2vw 5vw;
height: 8vh;
}
.hourly [class^='hourly'] {
padding-left: 1vw;
width: 12.89vw;
height: 8vh;
float: left;
text-align: center;
margin-top: 60px;
}
.hourly [class^='hourly']:not([class$='0']) {
border-left: 1px solid white;
}
.hourly [class^='hourly'] .icon {
height: 55px;
}
.extended {
height: 12vh;
margin: 2vw;
padding: 2vw 5vw;
margin-top: 80px;
}
.extended div:not([class='value']) {
width: 40vw;
height: 6vh;
float: left;
padding: 1vw;
text-align: center;
}
.extended div span {
font-size: 22px;
}
.extended div div {
margin-top: 10px;
margin-bottom: 20px;
}
.forecast {
height: 25vh;
margin: 2vw;
padding: 2vw 5vw;
}
.forecast [class^='forecast'] * {
display: inline-block;
margin-top: 10px;
}
.forecast [class^='forecast'] .weekday {
margin-left: 3vw;
width: 31vw;
font-size: 19px;
}
.forecast [class^='forecast'] .icon {
width: 5vw;
position: relative; top: 10px; right: 18px;
}
.forecast [class^='forecast'] .humid {
width: 22vw;
}
.forecast [class^='forecast'] .temp_high {
width: 11vw;
text-align: left;
}
.forecast [class^='forecast'] .temp_high i {
position: relative; top: 4px; right: 5px;
}
.forecast [class^='forecast'] .temp_low {
width: 9vw;
margin-right: 3vw;
text-align: left;
}
.forecast [class^='forecast'] .temp_low i {
position: relative; top: 4px; right: 5px;
}
React: back-end
There is no front-end without back-end, right?
./src/main/back/backDateTime.js
function pad(num, size) {
return ('000000000' + num).substr(-size);
}
function nth(day) {
if (day > 3 && day < 21)
return 'th';
switch (day % 10) {
case 1:
return 'st';
case 2:
return 'nd';
case 3:
return 'rd';
default:
return 'th';
}
}
class backDateTime {
constructor() {
this.date = new Date();
this.dateInterval = setInterval(() => {
this.date = new Date();
}, 1000);
this.format = 'en-us';
}
destroy() {
clearInterval(this.dateInterval);
}
getTime() {
// TO DO: check settings for a clock type (12/24)
let time = {};
time.hours = this.date.toLocaleTimeString(this.format, {hour12: false, hour: '2-digit'});
time.minutes = pad(this.date.getMinutes(), 2);
time.seconds = pad(this.date.getSeconds(), 2);
//time.divide = this.date.toLocaleTimeString(this.format, {hour12: true, hour: '2-digit'}).slice(2);
time.divide = '';
time.full = time.hours + ':' + time.minutes
return time;
}
getDate() {
// TO DO: check settings for a date arrangement
let date = {};
date.day = this.date.getDate();
date.year = this.date.getFullYear();
date.month = this.date.getMonth();
date.ordinal = nth(this.date.getDate());
date.weekday = this.date.toLocaleDateString(this.format, {weekday: 'long'});
date.monthname = this.date.toLocaleDateString(this.format, {month: 'long'});
date.full = date.day + date.ordinal + ' ' + date.monthname;
return date;
}
}
export default backDateTime;
./src/main/back/backCalendar.js
class backCalendar {
readClientSecret() {
return new Promise(resolve => {
let fs = window.require('fs');
fs.readFile('/walle/client_secret.json', (err, content) => {
if (err) {
console.log('Error loading client secret file');
console.log(err);
return false;
}
resolve(JSON.parse(content));
});
});
}
readAccessToken() {
return new Promise(resolve => {
let fs = window.require('fs');
fs.readFile('/walle/access_token.json', (err, content) => {
if (err) {
console.log('Error loading access token file');
console.log(err);
return false;
}
resolve(JSON.parse(content));
});
});
}
async getUpcomingEvents() {
let client_secret = await this.readClientSecret();
let access_token = await this.readAccessToken();
let date = new Date();
let events = await this.getSpecificEvents(client_secret, access_token, date.getFullYear(), date.getMonth()+1, date.getDate(), 'primary', false, 3);
return events;
}
async getMonthEvents(year, month) {
Date.prototype.monthDays = function() {
var d = new Date(this.getFullYear(), this.getMonth()+1, 0);
return d.getDate();
}
let client_secret = await this.readClientSecret();
let access_token = await this.readAccessToken();
let date = new Date(year, month-1);
let monthDays = date.monthDays();
let events = [];
for (var i=1; i<=monthDays; i++) {
let dayEvents = await this.getSpecificEvents(client_secret, access_token, year, month, i, 'primary', true, 3);
events.push(dayEvents);
}
return events;
}
async getDayEvents(year, month, day) {
let client_secret = await this.readClientSecret();
let access_token = await this.readAccessToken();
let events = await this.getSpecificEvents(client_secret, access_token, year, month, day, 'primary', true, 100);
return events;
}
async getSpecificEvents(credentials, token, year, month, day, calendarId, allDay, resultsLimit=20) {
var google = window.require('googleapis');
var googleAuth = window.require('google-auth-library');
let clientSecret = credentials.installed.client_secret;
let clientId = credentials.installed.client_id;
let redirectUrl = credentials.installed.redirect_uris[0];
let auth = new googleAuth();
let oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl);
oauth2Client.credentials = token;
return new Promise(resolve => {
var calendar = google.calendar('v3');
let date = new Date();
let timezone = -1 * date.getTimezoneOffset();
var specific_start;
if (allDay)
specific_start = new Date(year, month-1, day, 0, 0+timezone, 0);
else
specific_start = new Date(year, month-1, day, date.getHours(), date.getMinutes()+timezone, date.getSeconds());
var specific_end = new Date(year, month-1, day, 23, 59, 59);
if (calendarId !== 'primary')
calendarId = encodeURIComponent(calendarId);
calendar.events.list({
auth: oauth2Client,
calendarId: 'primary'
}, {
qs: {
timeMin: specific_start.toISOString(),
timeMax: specific_end.toISOString(),
maxResults: 100,
singleEvents: true,
orderBy: 'startTime'
}
}, (err, response) => {
if (err) {
console.log('Error reading events from Google Calendar:');
console.log(err);
return false;
}
let events = response.items;
let calendarEvents = {};
calendarEvents.requestYear = year;
calendarEvents.requestMonth = month;
calendarEvents.requestDay = day;
calendarEvents.quantity = events.length;
if (!calendarEvents.quantity) {
resolve(calendarEvents);
return;
}
calendarEvents.events = [];
for (var i=0; i<calendarEvents.quantity; i++) {
calendarEvents.events.push({
summary: events[i].summary,
creator_email: events[i].creator.email,
creator_name: events[i].creator.displayName,
start: {
date: {
year: events[i].start.dateTime.substr(0, 4),
month: events[i].start.dateTime.substr(5, 2),
day: events[i].start.dateTime.substr(8, 2)
},
time: {
hours: events[i].start.dateTime.substr(11, 2),
minutes: events[i].start.dateTime.substr(14, 2),
seconds: events[i].start.dateTime.substr(17, 2)
}
},
end: {
date: {
year: events[i].end.dateTime.substr(0, 4),
month: events[i].end.dateTime.substr(5, 2),
day: events[i].end.dateTime.substr(8, 2)
},
time: {
hours: events[i].end.dateTime.substr(11, 2),
minutes: events[i].end.dateTime.substr(14, 2),
seconds: events[i].end.dateTime.substr(17, 2)
}
}
});
}
resolve(calendarEvents);
});
});
}
}
export default backCalendar;
./src/main/back/backWeather.js
function pad(num, size) {
return ('000000000' + num).substr(-size);
}
function nth(day) {
if (day > 3 && day < 21)
return 'th';
switch (day % 10) {
case 1:
return 'st';
case 2:
return 'nd';
case 3:
return 'rd';
default:
return 'th';
}
}
function monthname(day) {
let months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
return months[day-1];
}
function convertTo24Hour(time) {
var hours = parseInt(time.substr(0, 2));
if(time.indexOf('AM') !== -1 && hours === 12)
time = time.replace('12', '0');
if(time.indexOf('PM') !== -1 && hours < 12)
time = time.replace(hours, (hours + 12));
return time.replace(/(AM|PM)/, '').trim();
}
class backWeather {
constructor() {
this.apiKey = 'your_api_key';
this.zmw = '00000.375.12660';
}
getConditionImg(condition) {
return './icons/weather/mostly_cloudy.svg';
}
async getToday() {
let body = await new Promise(resolve => {
fetch('http://api.wunderground.com/api/' + this.apiKey + '/conditions/q/zmw:' + this.zmw + '.json')
.then(res => {
resolve(res.json());
})
});
let today = {};
today.loc_city = body.current_observation.display_location.city;
today.loc_state = body.current_observation.display_location.state;
today.loc_country = body.current_observation.display_location.state_name;
// TO DO: CONVERT UTC TO LOCAL TIME
let date = new Date(body.current_observation.observation_epoch*1000);
today.observation = {};
today.observation.time = {};
today.observation.time.hours = date.toLocaleTimeString('en-us', {hour12: false, hour: '2-digit'});
today.observation.time.minutes = pad(date.getMinutes(), 2);
today.observation.time.seconds = pad(date.getSeconds(), 2);
today.observation.time.divide = date.toLocaleTimeString('en-us', {hour12: true, hour: '2-digit'}).slice(2);
today.observation.date = {};
today.observation.date.day = date.getDate();
today.observation.date.ordinal = nth(today.observation.date.day);
today.observation.date.weekday = date.toLocaleDateString('en-us', {weekday: 'long'});
today.observation.date.monthname = monthname(date.getMonth()+1);
today.observation.date.full = today.observation.date.day + today.observation.date.ordinal + ' ' + today.observation.date.monthname;
today.temp = Math.round(body.current_observation.temp_c);
today.feelslike = Math.round(body.current_observation.feelslike_c);
today.temp_unit = '°C';
today.humidity = body.current_observation.relative_humidity;
today.wind = body.current_observation.wind_kph;
today.wind_unit = 'kph';
today.wind_dir = body.current_observation.wind_dir;
today.pressure = body.current_observation.pressure_mb;
today.pressure_unit = 'hPa';
today.condition = body.current_observation.weather;
today.img = this.getConditionImg(today.condition);
body = await new Promise(resolve => {
fetch('http://api.wunderground.com/api/' + this.apiKey + '/forecast/q/zmw:' + this.zmw + '.json')
.then(res => {
resolve(res.json());
})
});
today.temp_high = body.forecast.simpleforecast.forecastday[0].high.celsius;
today.temp_low = body.forecast.simpleforecast.forecastday[0].low.celsius;
return today;
}
async getHourly() {
let body = await new Promise(resolve => {
fetch('http://api.wunderground.com/api/' + this.apiKey + '/hourly/q/zmw:' + this.zmw + '.json')
.then(res => {
resolve(res.json());
})
});
let hourly = [];
for (var i=0; i<6; i++) {
hourly.push({
'time': convertTo24Hour(body.hourly_forecast[i].FCTTIME.civil),
'temp': body.hourly_forecast[i].temp.metric,
'temp_unit': '°C',
'condition': body.hourly_forecast[i].condition,
'condition_img': this.getConditionImg(body.hourly_forecast[i].condition)
});
}
return hourly;
}
async getForecast() {
let body = await new Promise(resolve => {
fetch('http://api.wunderground.com/api/' + this.apiKey + '/forecast10day/q/zmw:' + this.zmw + '.json')
.then(res => {
resolve(res.json());
})
});
let forecast = [];
for (var i=0; i<9; i++) {
forecast.push({
'weekday': body.forecast.simpleforecast.forecastday[i].date.weekday,
'temp_high': body.forecast.simpleforecast.forecastday[i].high.celsius,
'temp_low': body.forecast.simpleforecast.forecastday[i].low.celsius,
'temp_unit': '°C',
'condition': body.forecast.simpleforecast.forecastday[i].conditions,
'condition_img': this.getConditionImg(body.forecast.simpleforecast.forecastday[i].conditions),
'wind_ave': body.forecast.simpleforecast.forecastday[i].avewind.kph,
'wind_dir': body.forecast.simpleforecast.forecastday[i].avewind.dir,
'wind_unit': 'kph',
'humidity_ave': body.forecast.simpleforecast.forecastday[i].avehumidity + '%',
'pop': body.forecast.simpleforecast.forecastday[i].pop,
'rain_day': body.forecast.simpleforecast.forecastday[i].qpf_allday.mm,
'rain_unit': 'mm',
'snow_day': body.forecast.simpleforecast.forecastday[i].snow_allday.mm,
'snow_unit': 'mm'
});
}
return forecast;
}
}
export default backWeather;
./src/main/back/backNews.js
function nth(day) {
if (day > 3 && day < 21)
return 'th';
switch (day % 10) {
case 1:
return 'st';
case 2:
return 'nd';
case 3:
return 'rd';
default:
return 'th';
}
}
function monthname(day) {
let months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
return months[day-1];
}
class backNews {
constructor() {
this.source = 'the-verge';
this.apiKey = 'your_api_key';
}
getSourceOfNews(source) {
if (source === 'the-verge')
return 'The Verge';
}
async getLatest() {
let body = await new Promise(resolve => {
fetch('https://newsapi.org/v1/articles?source=' + this.source + '&sortBy=latest&apiKey=' + this.apiKey)
.then(res => {
resolve(res.json());
})
});
let latest = {};
let art_num = 9;
latest.source = this.getSourceOfNews(this.source);
latest.author = body.articles[art_num].author;
latest.title = body.articles[art_num].title;
latest.desc = body.articles[art_num].description;
latest.url = body.articles[art_num].url;
// TO DO: CONVERT UTC TO LOCAL TIME
latest.publishedAt = {};
latest.publishedAt.day = body.articles[art_num].publishedAt.substr(8, 2);
latest.publishedAt.ordinal = nth(latest.publishedAt.day);
latest.publishedAt.monthname = monthname(body.articles[art_num].publishedAt.substr(5, 2));
latest.publishedAt.year = body.articles[art_num].publishedAt.substr(0, 4);
latest.publishedAt.hours = body.articles[art_num].publishedAt.substr(11, 2);
latest.publishedAt.minutes = body.articles[art_num].publishedAt.substr(14, 2);
latest.publishedAt.seconds = body.articles[art_num].publishedAt.substr(17, 2);
return latest;
}
}
export default backNews;
./src/main/back/backHome.js
class backHome {
constructor() {
this.serial = '/dev/ttyUSB0';
}
connectWithController(message, serial=this.serial) {
return new Promise(resolve => {
let PythonShell = window.require('python-shell');
let options = {
mode: 'text',
pythonPath: '/usr/bin/python',
scriptPath: '/walle/',
args: [serial, message]
};
PythonShell.run('walle_controller.py', options, (err, results) => {
if (err) throw err;
resolve(results[0]);
});
});
}
saveStatusInFile(file, status) {
let fs = window.require('fs');
fs.writeFile(file, status, (err) => {
if (err) {
console.log('Error writing status to file ' + file);
return false;
}
})
}
readStatusFromFile(file) {
return new Promise(resolve => {
let fs = window.require('fs');
fs.readFile(file, (err, content) => {
if (err) {
console.log('Error reading status from file ' + file);
return false;
}
resolve(content.toString());
});
});
}
async checkLight() {
let body = await this.readStatusFromFile('/walle/lightStatus.txt');
return body;
}
async lightOn() {
let body = await this.connectWithController('M1H');
if (body === 'True') {
this.saveStatusInFile('/walle/lightStatus.txt', 'true');
return true;
}
else if (body === 'False') {
this.saveStatusInFile('/walle/lightStatus.txt', 'false');
return false;
}
}
async lightOff() {
let body = await this.connectWithController('M1L');
if (body === 'True') {
this.saveStatusInFile('/walle/lightStatus.txt', 'false');
return true;
}
else if (body === 'False') {
this.saveStatusInFile('/walle/lightStatus.txt', 'true');
return false;
}
}
async checkCentralHeating() {
let body = await this.readStatusFromFile('/walle/centralHeatingStatus.txt');
return body;
}
async centralHeatingOn() {
let body = await this.connectWithController('M2H');
if (body === 'True') {
this.saveStatusInFile('/walle/centralHeatingStatus.txt', 'true');
return true;
}
else if (body === 'False') {
this.saveStatusInFile('/walle/centralHeatingStatus.txt', 'false');
return false;
}
}
async centralHeatingOff() {
let body = await this.connectWithController('M2L');
if (body === 'True') {
this.saveStatusInFile('/walle/centralHeatingStatus.txt', 'false');
return true;
}
else if (body === 'False') {
this.saveStatusInFile('/walle/centralHeatingStatus.txt', 'true');
return false;
}
}
async readCurrentTemp() {
let body = await new Promise(resolve => {
let fs = window.require('fs');
fs.readFile('/walle/currentTemp.txt', (err, content) => {
if (err) {
console.log('Error reading status from file currentTemp.txt');
return false;
}
resolve(parseInt(content.toString()));
});
});
return body;
}
async readTargetTemp() {
let body = await new Promise(resolve => {
let fs = window.require('fs');
fs.readFile('/walle/targetTemp.txt', (err, content) => {
if (err) {
console.log('Error reading status from file targetTemp.txt');
return false;
}
resolve(parseInt(content.toString()));
});
});
return body;
}
}
export default backHome;
Our JavaScript back-end uses a few APIs, so let's create all accounts and prepare API-keys.
Prepare a special directory for all our files and scripts
You must create a new folder with name walle in the root directory on your microSD card and set the permissions. Type these commands:
sudo mkdir /walle
sudo chmod 777 /walle
API: Google Calendar
You need to turn on Google Calendar API, install neccessary npm modules, set up sample application and get two keys: client_secret.json and access_token.json. Just follow the instructions inside Official Google Node.js Quickstart and you will get everything what you need (remember to move it to /walle/ directory).
API: Weather Wunderground
Let's create a new account with a free plan on the Wunderground Website, get API key and place it inside this.apiKey object - it's inside constructor of backWeather class.
API: News
Create a standard free account on the News API Website, get API key and place it inside this.apiKey object - it's inside constructor of backNews class.
Python script for Walle Controllers
It's easy script for communicating Walle Controllers with our XBee USB Explorer using a standard serial port.
/walle/walle_controller.py
import serial
import sys
def send_message(serial, message):
serial.write(message.encode())
response = serial.read(len(message))
if response and response == message:
return True
else:
count = 0
while not response and count < 3:
count += 1
response = serial.read(len(message))
if response and response == message:
return True
else:
return False
ser = serial.Serial(sys.argv[1], 9600, timeout=10)
print(send_message(ser, sys.argv[2]))
ser.close()
Text files for status and temperature
Alexa and JavaScript back-end uses the same files for reading and writing status of Walle Controllers. Let's create them by typing these commands:
sudo touch /walle/lightStatus.txt
sudo touch /walle/centralHeatingStatus.txt
sudo touch /walle/currentTemp.txt
sudo tocuh /walle/targetTemp.txt
Connect other parts
We use DHT11 electronic part for reading indoor temperature. Let's connect it as here:
Of course, we need also a Python script, which will be running in background, reading and saving indoor temperature all the time. But before making a script we need to download a library for DHT11 sensor.
cd ~
sudo apt-get install git-core
git clone https://github.com/adafruit/Adafruit_Python_DHT.git
cd Adafruit_Python_DHT
sudo apt-get install build-essential python-dev
sudo python setup.py install
~/save_temp.py (ALERT: VERY DIFFICULT!)
import Adafruit_DHT
while True:
hum, temp = Adafruit_DHT.read_retry(11, 4)
file = open('/walle/currentTemp.txt', 'w')
file.write(str(temp))
file.close()
Almost final part: integrating Amazon Alexa
Firstly, you must register a new product. Just follow the instructions from Amazon Official Tutorial.
Secondly, install some neccessary dependences:
sudo apt-get install python2.7-dev python-dev python-pip
sudo pip install Flask flask-ask
Get the latest Linux ARM release of Ngrok from Ngrok Official Website and unzip it inside your home directory (~/ or /home/pi). Run it by typing sudo ./ngrok http 5000 command. Don't close this window, because after some time you will have to provide ngrok url address (this one forwarding section).
Of course, something has to listen for requests and give responses to Alexa. That's why we need to create another Python script (yeah, I love Python).
~/alexa-control.py
from flask import Flask
from flask_ask import Ask, statement, convert_errors
import logging
import serial
app = Flask(__name__)
ask = Ask(app, '/')
logging.getLogger("flask_ask").setLevel(logging.DEBUG)
def send_message(message):
ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=10)
ser.write(message.encode())
response = ser.read(len(message))
if response and response == message:
ser.close()
return True
else:
count = 0
while not response and count < 3:
count += 1
response = ser.read(len(message))
ser.close()
if response and response == message:
return True
else:
return False
def save_status(file, status):
f = open(file, 'w')
f.write(status)
f.close()
def read_status(file):
f = open(file, 'r')
status = f.read()
f.close()
return status
@ask.intent('LightIntent', mapping={'status': 'status'})
def light_intent(status):
if status == 'on':
if send_message('M1H'):
save_status('/walle/lightStatus.txt', 'true')
else:
save_status('/walle/lightStatus.txt', 'false')
elif status == 'off':
if send_message('M1L'):
save_status('/walle/lightStatus.txt', 'false')
else:
save_status('/walle/lightStatus.txt', 'true')
return statement('Turning the light {}'.format(status))
@ask.intent('LightInfoIntent', mapping={'status': 'status'})
def light_info_intent(status):
if read_status('/walle/lightStatus.txt') == 'true':
return statement('The light is turned on')
elif read_status('/walle/lightStatus.txt') == 'false':
return statement('The light is turned off')
@ask.intent('CentralHeatingIntent', mapping={'status': 'status'})
def central_heating_intent(status):
if status == 'on':
if send_message('M2H'):
save_status('/walle/centralHeatingStatus.txt', 'true')
else:
save_status('/walle/centralHeatingStatus.txt', 'false')
elif status == 'off':
if send_message('M2L'):
save_status('/walle/centralHeatingStatus.txt', 'false')
else:
save_status('/walle/centralHeatingStatus.txt', 'true')
return statement('Turning the central heating {}'.format(status))
@ask.intent('CentralHeatingInfoIntent', mapping={'status': 'status'})
def central_heating_info_intent(status):
if read_status('/walle/centralHeatingStatus.txt') == 'true':
return statement('Central heating system is turned on')
elif read_status('/walle/centralHeatingStatus.txt') == 'false':
return statement('Central heating system is turned off')
@ask.intent('TargetTempIntent', mapping={'temperature': 'temperature'})
def target_temp_intent(temperature):
save_status('/walle/targetTemp.txt', temperature)
return statement('Setting the target temperature to {} degrees'.format(temperature))
@ask.intent('TargetTempInfoIntent')
def target_temp_info_intent(temperature):
tmp = read_status('/walle/targetTemp.txt')
return statement('The target temperature is set to {} degrees'.format(tmp))
@ask.intent('IndoorTempInfoIntent')
def indoor_temp_info_intent(temperature):
tmp = read_status('/walle/currentTemp.txt')
return statement('The indoor temperature is set to {} degrees'.format(tmp))
if __name__ == '__main__':
port = 5000
app.run(host='0.0.0.0', port=port)
Now let's create a free account on Amazon Developer Website, because we need to have an access to the Developer Console. In the developer console, select 'Alexa' and 'Alexa Skill Set' as shown in the snippet. On the next page select 'Add a new skill' on the right hand side.
Set the Skill Name and Invocation Name to Walle.
Inside Interaction Model tab add these things...
Intent Schema
{
"intents": [
{
"slots": [
{
"name": "status",
"type": "ON_OFF"
}
],
"intent": "LightIntent"
},
{
"slots": [
{
"name": "status",
"type": "ON_OFF"
}
],
"intent": "LightInfoIntent"
},
{
"slots": [
{
"name": "status",
"type": "ON_OFF"
}
],
"intent": "CentralHeatingIntent"
},
{
"slots": [
{
"name": "status",
"type": "ON_OFF"
}
],
"intent": "CentralHeatingInfoIntent"
},
{
"slots": [
{
"name": "temperature",
"type": "AMAZON.NUMBER"
}
],
"intent": "TargetTempIntent"
},
{
"intent": "TargetTempInfoIntent"
},
{
"intent": "IndoorTempInfoIntent"
}
]
}
Add new slot type with type ON_OFF and values as following:
on
of
Add a lot of Sample Utterances in case of intrusive users of Walle:
LightIntent to turn the light {status}
LightIntent to turn my light {status}
LightIntent to switch the light {status}
LightIntent to switch my light {status}
LightIntent to turn {status} the light
LightIntent to turn {status} my light
LightIntent to switch {status} the light
LightIntent to switch {status} my light
LightInfoIntent if the light is turned {status}
LightInfoIntent if my light is turned {status}
LightInfoIntent whether the light is turned {status}
LightInfoIntent whether my light is turned {status}
CentralHeatingIntent to turn heating {status}
CentralHeatingIntent to turn central heating {status}
CentralHeatingIntent to turn central heating system {status}
CentralHeatingIntent to switch central heating {status}
CentralHeatingIntent to switch central heating system {status}
CentralHeatingIntent to turn {status} central heating
CentralHeatingIntent to turn {status} central heating system
CentralHeatingIntent to switch {status} central heating
CentralHeatingIntent to switch {status} central heating system
CentralHeatingInfoIntent if central heating is turned {status}
CentralHeatingInfoIntent if central heating system is turned {status}
CentralHeatingInfoIntent whether central heating is turned {status}
CentralHeatingInfoIntent whether central heating system is turned {status}
TargetTempIntent to set target temperature to {temperature}
TargetTempIntent to set target temperature to {temperature} degrees
TargetTempIntent to set central heating temperature to {temperature}
TargetTempIntent to set central heating temperature to {temperature} degrees
TargetTempIntent to set central heating system temperature to {temperature}
TargetTempIntent to set central heating system temperature to {temperature} degrees
TargetTempInfoIntent what is the target temperature
TargetTempInfoIntent what's the target temperature
TargetTempInfoIntent what is the temperature of central heating
TargetTempInfoIntent what's the temperature of central heating
TargetTempInfoIntent what is the temperature of central heating system
TargetTempInfoIntent what's the temperature of central heating system
TargetTempInfoIntent what temperature is set on central heating system
IndoorTempInfoIntent what is the indoor temperature
IndoorTempInfoIntent what is the room temperature
IndoorTempInfoIntent what's the indoor temperature
IndoorTempInfoIntent what's the room temperature
Inside Configuration and SSL tab select HTTPS as the Service Endpoint Type, select North America as region and provide the url website from ngrok terminal window (it's very important).
Is that all?
Hahah, no. ;)
If you have got your own Amazon Echo it's cool, because you just need to log in into your account and you can control your home automation. But wait! Yeah, I didn't provide you electronic schematics. You must wait a while.
Build your own Amazon Echo
No, we won't create any standalone device. We're going to just install Alexa Voice Service on the Raspberry Pi and we will have own Amazon Echo for free. Let's do it quickly:
wget https://raw.githubusercontent.com/alexa/avs-device-sdk/master/tools/RaspberryPi/setup.sh && wget
Now update config.txt with the Client ID, Client Secret, and Product ID (you can get them from Amazon Developer Console) for your registered product and save it.
sudo bash setup.sh
sudo bash startauth.sh
Enable your Amazon Echo with a wake word
Just follow the step 7 inside Offical Amazon Tutorial.
Use online simulator of Amazon Echo
If you want to be like me and you don't need to install AVS on Raspberry or buy a real Amazon Echo, use Alexa Skills Kit Simulator. I'm using it on the videos, but I haven't showed it.
Walle Controllers - how to build them?
We have built two Walle Controllers - for the light and central heating system.
Special Thanks toDawid Żyrek - our Electronic Hardware Developer!
Now I'm providing it to you:
Walle Controller - The Light
Walle Controller - The Light - Arduino C code
int deviceID = 1;
String deviceH = 'M' + String(deviceID) + 'H';
String deviceL = 'M' + String(deviceID) + 'L';
void setup() {
Serial.begin(9600);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(2, OUTPUT);
}
void relayOn() {
digitalWrite(LED_BUILTIN, HIGH);
digitalWrite(2, HIGH);
Serial.print(deviceH);
}
void relayOff() {
digitalWrite(LED_BUILTIN, LOW);
digitalWrite(2, LOW);
Serial.print(deviceL);
}
void loop() {
while (Serial.available()) {
String message = Serial.readString();
if (message == deviceH)
relayOn();
else if (message == deviceL)
relayOff();
}
}
Walle Controller - Central Heating System
Walle Controller - Central Heating - Arduino C code
int deviceID = 2;
String deviceH = 'M' + String(deviceID) + 'H';
String deviceL = 'M' + String(deviceID) + 'L';
void setup() {
Serial.begin(9600);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(2, OUTPUT);
}
void relayOn() {
digitalWrite(LED_BUILTIN, HIGH);
digitalWrite(2, HIGH);
Serial.print(deviceH);
}
void relayOff() {
digitalWrite(LED_BUILTIN, LOW);
digitalWrite(2, LOW);
Serial.print(deviceL);
}
void loop() {
while (Serial.available()) {
String message = Serial.readString();
if (message == deviceH)
relayOn();
else if (message == deviceL)
relayOff();
}
}
XBee modules: configuration
The idea of the Walle - Home Assistant project is to have the main device (assistant) in centre of home on the wall and to control for example central heating system. We couldn't use Bluetooth or Wi-Fi, because we were afraid of the signal range.
We have used XBee communication modules, which are working on the ZigBee protocol (IEEE 802.15.4). Despite only 1mW output signal and small U.FL antenna they can communicate between each other even when they are roughly 25 metres (through the all walls inside the house)!
We recommend to configure all ZigBee modules in the same PAN network. Let's download XCTU from Official Digi Website and configure them all.
Connect the master XBee module (this one inside assistant) and configure as following. How? Recover it to ZigBee Coordinator AT, set the PAN ID to 1234 and Destination Address to 0xFFFF (broadcast).
Connect the slaves XBee modules (these are inside Walle Controllers) and configure each as following. How? Recover it to ZigBee End Device AT, set the PAN ID to 1234 and Destination Address to 0x0 (PAN Coordinator address).
That's all!
Thank you very much! I'm so glad you managed to read this whole page. I know this project isn't easy, therefore I'm leaving you my contact information. I will respond to even silly question. I won't bite. ;)
You can contact me using Facebook, Twitter, LinkedIn.
I'm still going to work on this project. I have got a lot of new ideas and different ways how develop my system. Stay tuned!
Leave your feedback...