Valve control #3

Merged
senaduka merged 10 commits from valve_control into master 2016-01-24 10:31:26 +01:00
27 changed files with 444 additions and 57 deletions

View File

@@ -22,3 +22,5 @@ less
huttonr:bootstrap3 huttonr:bootstrap3
nimble:restivus nimble:restivus
momentjs:moment momentjs:moment
selaias:meteor-simpleweather
u2622:persistent-session

View File

@@ -1,4 +1,5 @@
accounts-base@1.2.2 accounts-base@1.2.2
amplify@1.0.0
autopublish@1.0.4 autopublish@1.0.4
autoupdate@1.2.4 autoupdate@1.2.4
babel-compiler@5.8.24_1 babel-compiler@5.8.24_1
@@ -27,6 +28,7 @@ ejson@1.0.7
es5-shim@4.1.14 es5-shim@4.1.14
fastclick@1.0.7 fastclick@1.0.7
geojson-utils@1.0.4 geojson-utils@1.0.4
handlebars@1.0.4
hot-code-push@1.0.0 hot-code-push@1.0.0
html-tools@1.0.5 html-tools@1.0.5
htmljs@1.0.5 htmljs@1.0.5
@@ -36,6 +38,7 @@ huttonr:bootstrap3-assets@3.3.5_6
id-map@1.0.4 id-map@1.0.4
insecure@1.0.4 insecure@1.0.4
jquery@1.11.4 jquery@1.11.4
json@1.0.3
launch-screen@1.0.4 launch-screen@1.0.4
less@2.5.1 less@2.5.1
livedata@1.0.15 livedata@1.0.15
@@ -62,6 +65,7 @@ reactive-var@1.0.6
reload@1.1.4 reload@1.1.4
retry@1.0.4 retry@1.0.4
routepolicy@1.0.6 routepolicy@1.0.6
selaias:meteor-simpleweather@0.6.8
service-configuration@1.0.5 service-configuration@1.0.5
session@1.1.1 session@1.1.1
simple:json-routes@1.0.4 simple:json-routes@1.0.4
@@ -71,6 +75,7 @@ standard-minifiers@1.0.2
templating@1.1.5 templating@1.1.5
templating-tools@1.0.0 templating-tools@1.0.0
tracker@1.0.9 tracker@1.0.9
u2622:persistent-session@0.4.4
ui@1.0.8 ui@1.0.8
underscore@1.0.4 underscore@1.0.4
url@1.0.5 url@1.0.5

View File

@@ -1,25 +1,20 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>TFM</title> <title>TFM</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" content="Amir Smajevic, Adnan Strojil, Senad Uka"> <meta name="author" content="Amir Smajevic, Adnan Strojil, Senad Uka">
</head> </head>
<body> <body>
<ul class="nav nav-tabs"> {{> tabs}}
<li role="presentation" class="active"><a href="#">Početak</a></li>
<li role="presentation"><a href="#">Novosti</a></li>
</ul>
<div class="container-fluid"> <div class="container-fluid">
<div class="hello"> {{> Template.dynamic template=template_name }}
{{> display}} </div>
</div>
</div> <!-- /container -->
</body>
<!-- /container -->
</body>

View File

@@ -1,17 +1,8 @@
if (Meteor.isClient) { // at the beginning
Session.set("templateName", "start");
Template.display.helpers({ Template.body.helpers({
sensorDataCollection: function () { template_name: function() {
return SensorData.find({}, {sort: {created_at: -1}}) return Session.get("templateName");
} }
}); });
Template.display.events({
});
Template.sensorData.helpers({
created_at_formatted: function() {
return moment(this.created_at).format("DD.MM.YYYY, HH:mm")
}
});
}

View File

@@ -2,3 +2,7 @@
padding: 40px 15px; padding: 40px 15px;
text-align: center; text-align: center;
} }
.controller_selection {
padding-top: 10px;
}

View File

@@ -1,7 +0,0 @@
<template name="display">
<ul>
{{#each sensorDataCollection}}
{{> sensorData}}
{{/each}}
</ul>
</template>

13
app/client/log.html Normal file
View File

@@ -0,0 +1,13 @@
<template name="log">
<div class="hello">
<h1> Podaci sa senzora: </h1>
<button id="clear_log">Očisti</button>
<ul>
{{#each sensorDataCollection}}
{{> sensorData}}
{{/each}}
</ul>
</div>
</template>

21
app/client/log.js Normal file
View File

@@ -0,0 +1,21 @@
Template.log.helpers({
sensorDataCollection: function() {
return SensorData.find({}, {
sort: {
created_at: -1
}
});
}
});
Template.log.events({
'click clear_log': function() {
Meteor.call('clearLog');
}
});
Template.sensorData.helpers({
created_at_formatted: function() {
return moment(this.created_at).format("DD.MM.YYYY, HH:mm")
}
});

5
app/client/start.html Normal file
View File

@@ -0,0 +1,5 @@
<template name="start">
<div class="col-lg-12 text-center">
{{>state}}
</div>
</template>

17
app/client/start.js Normal file
View File

@@ -0,0 +1,17 @@
var options = {
location: 40.7127+','+ 74.0059, // New York
unit: 'c',
success: function(weather) {
html = '<h2><i class="sw icon-'+weather.code+'"></i> '
html += weather.temp+'&deg;'+weather.units.temp+'</h2>';
html += '<ul><li>'+weather.city+', '+weather.region +'</li>';
html += '<li class="currently">'+weather.currently+'</li>';
$("#weather").html(html);
},
error: function(error) {
$("#weather").html('<p>'+error+'</p>');
}
}
Weather.options = options

18
app/client/state.html Normal file
View File

@@ -0,0 +1,18 @@
<template name="state">
<h1> Controller state</h1>
<div>
{{#with controller_state}}
<div>Controller broj: {{controller_id}}</div>
<div>Otpusni ventil: {{pretty_valve state.out_valve }}</div>
<div>Stanje postavio: {{ set_by }}</div>
<div>Zadnja komunikacija: {{time}}</div>
{{/with}}
</div>
<div>
<button id="water_now">Zalij sada</button>
<button id="stop_water_now">Prekini zalijevanje</button>
</div>
</template>

52
app/client/state.js Normal file
View File

@@ -0,0 +1,52 @@
if (Meteor.isClient) {
function controller_state() {
var controller = Session.get('controller_id');
var result = {}
if (controller) {
result = ControllerState.findOne({
controller_id: controller
});
}
if (!result) {
result = {}
};
return result;
};
Template.state.helpers({
controller_state: controller_state,
pretty_valve: function(state) {
if (state === 'open') return "Otvoren";
if (state === 'opening') return "Otvara se";
if (state === 'closing') return "Zatvara se";
if (state === 'closed') return "Zatvoren";
}
});
Template.state.events({
'click #water_now': function() {
var state = controller_state();
ControllerState.update(state._id, {
'$set': {
'state.out_valve': 'opening',
'time': new Date(),
'set_by': 'server'
}
});
},
'click #stop_water_now': function() {
var state = controller_state();
ControllerState.update(state._id, {
'$set': {
'state.out_valve': 'closing',
'time': new Date(),
'set_by': 'server'
}
});
}
});
}

10
app/client/tabs.html Normal file
View File

@@ -0,0 +1,10 @@
<template name="tabs">
<ul class="nav nav-tabs">
<li role="presentation" class="{{ class_for 'start' }}"><a href="#">Stanje</a></li>
<li role="presentation" class="{{ class_for 'weather' }}"><a href="#">Vrijeme</a></li>
<li role="presentation" class="{{ class_for 'log' }}"><a href="#">Novosti</a></li>
<li role="presentation" class="controller_selection"> <input type="number" id="controller" name="controller" value="{{ selected_controller }}" min="1" max="99999"> <button id="switch" name="switch">Prebaci</button>
</li>
</ul>
</template>

39
app/client/tabs.js Normal file
View File

@@ -0,0 +1,39 @@
if (Meteor.isClient) {
Template.tabs.helpers({
class_for: function(tab_name) {
var templateName = Session.get('templateName');
if (templateName === tab_name) {
return tab_name + ' active';
} else if (templateName === 'display' && tab_name === 'news') {
return tab_name + ' active'
} else {
return tab_name;
}
},
selected_controller: function() {
return Session.get('controller_id');
},
});
Template.tabs.events({
'click .start': function() {
Session.set('templateName', 'start');
},
'click .weather': function() {
Session.set('templateName', 'weather');
},
'click .log': function() {
Session.set('templateName', 'log');
},
'click #switch': function() {
var instance = Template.instance();
controller_id = instance.$('#controller').val();
Session.setPersistent('controller_id', controller_id);
}
});
}

View File

@@ -1,2 +1,3 @@
SensorData = new Mongo.Collection("sensorData"); SensorData = new Mongo.Collection("sensorData");
ControllerState = new Mongo.Collection("controller_states");

61
app/server/api.js Normal file
View File

@@ -0,0 +1,61 @@
// Global API configuration
var Api = new Restivus({
useDefaultAuth: true,
prettyJson: true
});
Api.addRoute('sensorData', {
authRequired: false
}, {
post: function() {
SensorData.insert({
temperatureValue: parseFloat(this.bodyParams.temperatureValue),
humidityValue: parseFloat(this.bodyParams.humidityValue),
owner: this.bodyParams.owner,
created_at: new Date()
});
return [];
}
});
Api.addRoute('state/:id', {
authRequired: false
}, {
post: function() {
console.log("Body params", this.bodyParams);
return ControllerState.update({
controller_id: this.urlParams.id
}, {
'$set': {
'state.out_valve': this.bodyParams.out_valve,
'time': new Date(),
'set_by': 'client'
}
});
},
get: function() {
return stateOrDefault(this.urlParams.id).state;
}
});
function stateOrDefault(id) {
var stateEntry = ControllerState.findOne({
controller_id: id,
});
if (stateEntry === undefined) {
stateEntry = ControllerState.insert({
controller_id: id,
state: {
out_valve: 'closed'
},
time: new Date(),
set_by: 'server'
});
};
var stateEntry = ControllerState.findOne({
controller_id: id,
});
return stateEntry;
}

View File

@@ -1,6 +1,12 @@
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(function() { Meteor.startup(function() {
// code to run on server at startup // code to run on server at startup
return Meteor.methods({
clearLog: function() {
return SensorData.remove({});
}
});
}); });
// Global API configuration // Global API configuration
@@ -15,11 +21,13 @@ if (Meteor.isServer) {
post: function() { post: function() {
SensorData.insert({ SensorData.insert({
temperatureValue: parseFloat(this.bodyParams.temperatureValue), temperatureValue: parseFloat(this.bodyParams.temperatureValue),
humidityValue: parseFloat(this.bodyParams.humidityValue), humidityValue: parseFloat(this.bodyParams.humidityValue),
owner: this.bodyParams.owner, owner: this.bodyParams.owner,
created_at: new Date() created_at: new Date()
}); });
return []; return [];
} }
}); });
} }

View File

@@ -3,10 +3,12 @@
## Installation ## Installation
1. Go to every subdirectory in drivers directory and follow instructions about installation of drivers 1. Go to every subdirectory in drivers directory and follow instructions about installation of drivers
2. configure cron to run controller.py every 15 minutes as a superuser: 2. edit controller/config/__init__.py and set your controller ID to unique number
3. configure cron to run controller.py every 15 minutes as a superuser:
``` ```
crontab -e -u root crontab -e -u root
#enter this line #enter these lines
*/15 * * * * /usr/bin/python /home/pi/projects/tfm/controller/controller.py "Automatski, Senad Uka" */15 * * * * /usr/bin/python /home/pi/projects/tfm/controller/sensors.py "Automatski, Senad Uka" 120
*/1 * * * * /usr/bin/python /home/pi/projects/tfm/controller/sync_state.py "Automatski, Senad Uka" 120
``` ```

View File

@@ -0,0 +1,8 @@
GPIO_PIN_DHT = 4 # BCM
SENSORDATA_URL = 'http://tfm.meteor.com/api/sensorData'
GPIO_PIN_VALVE = 21 # BCM
API_BASE_URL = 'http://tfm.meteor.com/api'
CONTROLLER_ID = '120' # every controller must have a different one
STATE_FILE = '/var/run/controller_state'

View File

@@ -1 +1,2 @@
requests==2.4.3 requests==2.4.3
rpi.gpio==0.6.0a3

View File

@@ -2,22 +2,22 @@
import sys import sys
import requests import requests
import Adafruit_DHT import Adafruit_DHT
import config
GPIO_PIN = 4
SENSORDATA_URL = 'http://tfm.meteor.com/api/sensorData'
SENSOR_TYPE = Adafruit_DHT.DHT11 SENSOR_TYPE = Adafruit_DHT.DHT11
if len(sys.argv) == 2: if len(sys.argv) == 3:
owner = sys.argv[1] owner = sys.argv[1]
controller_id = sys.argv[2]
else: else:
print 'usage: sudo ./controller.py [OWNER]#' print 'usage: sudo ./controller.py [OWNER] [CONTROLLER_ID]#'
print 'example: sudo ./controller.py Senad - Send temperature as Senad' print 'example: sudo ./controller.py Senad 225 - Send temperature as Senad for controller 225'
sys.exit(1) sys.exit(1)
# Try to grab a sensor reading. Use the read_retry method which will retry up # Try to grab a sensor reading. Use the read_retry method which will retry up
# to 15 times to get a sensor reading (waiting 2 seconds between each retry). # to 15 times to get a sensor reading (waiting 2 seconds between each retry).
humidity, temperature = Adafruit_DHT.read_retry(SENSOR_TYPE, GPIO_PIN) humidity, temperature = Adafruit_DHT.read_retry(SENSOR_TYPE, config.GPIO_PIN_DHT)
# Un-comment the line below to convert the temperature to Fahrenheit. # Un-comment the line below to convert the temperature to Fahrenheit.
# temperature = temperature * 9/5.0 + 32 # temperature = temperature * 9/5.0 + 32
@@ -27,12 +27,11 @@ humidity, temperature = Adafruit_DHT.read_retry(SENSOR_TYPE, GPIO_PIN)
# guarantee the timing of calls to read the sensor). # guarantee the timing of calls to read the sensor).
# If this happens try again! # If this happens try again!
if temperature is not None and humidity is not None: if temperature is not None and humidity is not None:
response = requests.post(SENSORDATA_URL, json={"owner": owner, "temperatureValue": temperature, "humidityValue":humidity}) response = requests.post(config.SENSORDATA_URL, json={"owner": owner, "temperatureValue": temperature, "humidityValue":humidity})
print 'Temp={0:0.1f}*C'.format(temperature) print 'Temp={0:0.1f}*C'.format(temperature)
print 'Humidity={0:0.1f}%'.format(humidity) print 'Humidity={0:0.1f}%'.format(humidity)
if response.status_code != 200: if response.status_code != 200:
print 'Failed to send temperature!' print 'Failed to send temperature!'
sys.exit(2)
else: else:
print 'Failed to get reading. Try again!' print 'Failed to get reading. Try again!'
sys.exit(1)

View File

@@ -0,0 +1,27 @@
import config
from state.server import Server
from state.changer import Changer
from state.file import File
def sync():
server = Server(config.API_BASE_URL, config.CONTROLLER_ID)
local = File(config.STATE_FILE)
server_state = server.get_state()
if local.present():
local.load()
print "local present: " + repr(local.data)
else:
local.data = server_state
print "local not present, server: " + repr(local.data)
local.save()
local_state = local.data
changer = Changer(local_state, server_state)
current_state = changer.process_change()
server.post_state(current_state)

View File

@@ -0,0 +1,38 @@
import RPi.GPIO as GPIO
import config
class Changer(object):
def __init__(self, local_state, remote_state):
self.local_state = local_state
self.remote_state = remote_state
GPIO.setmode(GPIO.BCM) # Broadcom pin-numbering scheme
GPIO.setup(config.GPIO_PIN_VALVE, GPIO.OUT)
self.states = {
'opening': self.open_valve,
'closing': self.close_valve,
'open': self.open_valve,
'closed': self.close_valve
}
def process_change(self):
self.validate_states()
change = self.states.get(self.remote_state['out_valve'], None )
if change is not None:
change()
return self.local_state
def open_valve(self):
GPIO.output(config.GPIO_PIN_VALVE, GPIO.HIGH)
self.local_state['out_valve'] = 'open'
def close_valve(self):
GPIO.output(config.GPIO_PIN_VALVE, GPIO.LOW)
self.local_state['out_valve'] = 'closed'
def validate_states(self):
if self.local_state is None or self.remote_state is None:
raise ClassNotReadyException("Both local and remote states must be present!")
# TODO: add detailed validation

View File

@@ -0,0 +1,14 @@
class ClassNotReadyException(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
class ErrorCommunicatingWithServerException(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)

27
controller/state/file.py Normal file
View File

@@ -0,0 +1,27 @@
import json
import os.path
class File(object):
"Holds controller state in the file"
def __init__(self, filename=None):
self.filename = filename
def present(self):
os.path.isfile(self.filename)
def load(self):
if self.filename is None:
raise ClassNotReadyException("Filename not set!")
with open(self.filename) as input_file:
self.data = json.load(input_file)
def save(self):
if self.filename is None:
raise ClassNotReadyException("Filename not set!")
if self.data is None:
raise ClassNotReadyException("Data not loaded!")
with open(self.filename, 'w') as out_file:
json.dump(self.data, out_file)

View File

@@ -0,0 +1,33 @@
import json
import requests
class Server(object):
"Gets state from server and sends it to the server after change"
def __init__(self, url_base=None, controller_id=None):
self.url_base = url_base
self.controller_id = controller_id
def get_state(self):
result = requests.get(self.full_url('state/%s') % self.controller_id)
return handle_response(result)
def post_state(self, local_state):
result = requests.post(self.full_url('state/%s') % self.controller_id, local_state)
return handle_response(result)
def full_url(self, action):
if self.controller_id is None:
raise ClassNotReadyException("Controller id not set!")
if self.url_base is None:
raise ClassNotReadyException("URL base not set!")
return self.url_base + '/' + action
def handle_response(response):
if response.status_code != 200:
raise ErrorCommunicatingWithServerException("Response not 200!")
else:
return response.json()

3
controller/sync_state.py Normal file
View File

@@ -0,0 +1,3 @@
import state
state.sync()