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
nimble:restivus
momentjs:moment
selaias:meteor-simpleweather
u2622:persistent-session

View File

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

View File

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

View File

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

View File

@@ -2,3 +2,7 @@
padding: 40px 15px;
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");
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) {
Meteor.startup(function() {
// code to run on server at startup
return Meteor.methods({
clearLog: function() {
return SensorData.remove({});
}
});
});
// Global API configuration
@@ -15,11 +21,13 @@ if (Meteor.isServer) {
post: function() {
SensorData.insert({
temperatureValue: parseFloat(this.bodyParams.temperatureValue),
humidityValue: parseFloat(this.bodyParams.humidityValue),
humidityValue: parseFloat(this.bodyParams.humidityValue),
owner: this.bodyParams.owner,
created_at: new Date()
});
return [];
}
});
}

View File

@@ -3,10 +3,12 @@
## Installation
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
#enter this line
*/15 * * * * /usr/bin/python /home/pi/projects/tfm/controller/controller.py "Automatski, Senad Uka"
#enter these lines
*/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 requests
import Adafruit_DHT
import config
GPIO_PIN = 4
SENSORDATA_URL = 'http://tfm.meteor.com/api/sensorData'
SENSOR_TYPE = Adafruit_DHT.DHT11
if len(sys.argv) == 2:
if len(sys.argv) == 3:
owner = sys.argv[1]
controller_id = sys.argv[2]
else:
print 'usage: sudo ./controller.py [OWNER]#'
print 'example: sudo ./controller.py Senad - Send temperature as Senad'
print 'usage: sudo ./controller.py [OWNER] [CONTROLLER_ID]#'
print 'example: sudo ./controller.py Senad 225 - Send temperature as Senad for controller 225'
sys.exit(1)
# 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).
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.
# 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).
# If this happens try again!
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 'Humidity={0:0.1f}%'.format(humidity)
if response.status_code != 200:
print 'Failed to send temperature!'
sys.exit(2)
else:
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()