30 Commits

Author SHA1 Message Date
b7e734ac10 Fix error when no connection 2024-11-12 20:29:30 +01:00
3b3b6583d9 Fix bug with occasional wrong code 2024-11-10 11:12:36 +01:00
e6342d9c93 Shelly skripta 2024-11-09 17:55:28 +01:00
Senad Uka
68eb5c24f9 C based API 2024-10-13 16:21:50 +02:00
Senad Uka
a1cf3a40a3 Update gitignore 2024-10-12 17:59:33 +02:00
Senad Uka
3e01c0b7f7 Readme update 2019-07-28 12:07:18 +02:00
Senad Uka
82ea273c0d Sensor labels support 2019-07-28 11:48:50 +02:00
a5ae635e7d Update README.md 2017-11-16 20:33:07 +01:00
106b774098 Update README.md 2017-11-16 18:00:52 +01:00
048bc89b01 Update README.md 2017-11-16 17:58:57 +01:00
434c3a352e Update README.md 2017-06-18 00:32:05 +02:00
Senad Uka
620c69d1a0 sensordata now left to right 2017-06-04 16:02:26 +02:00
Senad Uka
f6e9d79e4b sensordata now limited to 100 again 2017-06-03 19:13:48 +02:00
Senad Uka
aba33781ef fixed temperatures hopefully 2017-05-24 19:12:57 +02:00
Senad Uka
947a9c6a43 added back watering route and hopefully fixed bug with graph 2017-05-24 18:45:37 +02:00
Senad Uka
81652c8c64 danas -> pregled 2017-05-21 17:55:24 +02:00
Senad Uka
e86ef0ba3a charts are up 2017-05-21 17:51:45 +02:00
8c236609e8 Merge pull request #38 from senaduka/temperature_errors_sensor_filtering
cancel button
2017-01-07 13:43:06 +01:00
Senad Uka
72a4b877a9 cancel button 2017-01-07 13:41:37 +01:00
546fc65842 Merge pull request #37 from senaduka/temperature_errors_sensor_filtering
Temperature errors sensor filtering
2017-01-07 13:34:22 +01:00
Senad Uka
53cd8264e8 silenced alarm notification 2017-01-07 13:27:35 +01:00
Senad Uka
977fb6c809 default on for sensor 2017-01-07 13:07:24 +01:00
Senad Uka
41ed24589b turning off sensor works now 2017-01-07 13:05:31 +01:00
Senad Uka
38a9bd83b9 saving sensor state works 2017-01-07 12:51:28 +01:00
Senad Uka
d1e216b0fd temperature sensors enabled / disabled ui 2017-01-07 12:40:21 +01:00
Senad Uka
a06a22857d temperature errors 2017-01-06 18:36:57 +01:00
411419c375 Merge pull request #36 from senaduka/cron_jobs_starting
cron jobs starting
2017-01-05 15:47:48 +01:00
Senad Uka
070a1071c9 cron jobs starting 2017-01-05 15:39:34 +01:00
8db2054f05 Merge pull request #35 from senaduka/farm_alarm_voice
Farm alarm voice
2017-01-05 15:25:38 +01:00
Senad Uka
b4da425241 configured twilio voice, fixed bug with added slash on numbers 2017-01-05 15:12:40 +01:00
39 changed files with 295565 additions and 81 deletions

5
.gitignore vendored
View File

@@ -60,3 +60,8 @@ target/
# meteor
app.tar.gz
.idea/
*.o
*.a
*.out

View File

@@ -6,3 +6,15 @@ TFM consists of following components:
* TFM Controller - daemon running on embedded platform for reading the sensors, executing commands in offline and online mode
# Deploying to server:
1. ssh to agrar.zoblak.com
2. do meteor build . on dev computer
3. cp app.tar.gz to zoblak@agrar.zoblak.com:/home/zoblak/
4. sudo su
5. cd
6. npm install -g node-gyp # for bcrypt to build correctly
7. npm install -g node-pre-gyp # for bcrypt to build correctly
6. ./deploy.sh

19
apiv2/Makefile Normal file
View File

@@ -0,0 +1,19 @@
CC = gcc
CFLAGS = -I. -I./include -I/opt/homebrew/include -I/usr/include -Wall $(shell pkg-config --cflags jansson)
SOURCES = src/main.c src/database.c mongoose.c sqlite3.c
OBJECTS = $(SOURCES:.c=.o)
TARGET = api_server
LIBS = -lpthread -ldl $(shell pkg-config --libs jansson)
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJECTS)
$(CC) $(CFLAGS) $(OBJECTS) -o $@ $(LIBS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJECTS) $(TARGET)

BIN
apiv2/api_server Executable file

Binary file not shown.

BIN
apiv2/database.db Normal file

Binary file not shown.

9
apiv2/include/database.h Normal file
View File

@@ -0,0 +1,9 @@
#ifndef DATABASE_H
#define DATABASE_H
int db_init();
char* db_query(const char *query);
int db_insert(const char *value);
void db_close();
#endif

19019
apiv2/mongoose.c Normal file

File diff suppressed because it is too large Load Diff

3166
apiv2/mongoose.h Normal file

File diff suppressed because it is too large Load Diff

257679
apiv2/sqlite3.c Normal file

File diff suppressed because it is too large Load Diff

13425
apiv2/sqlite3.h Normal file

File diff suppressed because it is too large Load Diff

88
apiv2/src/database.c Normal file
View File

@@ -0,0 +1,88 @@
#include "database.h"
#include <sqlite3.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <jansson.h>
static sqlite3 *db;
int db_init() {
int rc = sqlite3_open("database.db", &db);
if (rc) {
return rc;
}
// Initialize tables if necessary
const char *sql = "CREATE TABLE IF NOT EXISTS data (id INTEGER PRIMARY KEY, value TEXT);";
char *errmsg = 0;
rc = sqlite3_exec(db, sql, 0, 0, &errmsg);
if (rc != SQLITE_OK) {
sqlite3_free(errmsg);
}
return rc;
}
char* db_query(const char *query) {
// Prepare SQL statement
const char *sql = "SELECT * FROM data WHERE value LIKE ?;";
sqlite3_stmt *stmt;
char *response = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
return strdup("{\"error\": \"Failed to prepare statement\"}");
}
// Bind the query parameter
sqlite3_bind_text(stmt, 1, query, -1, SQLITE_TRANSIENT);
// Build JSON response
json_t *json_arr = json_array();
while (sqlite3_step(stmt) == SQLITE_ROW) {
json_t *json_obj = json_object();
int id = sqlite3_column_int(stmt, 0);
const unsigned char *value = sqlite3_column_text(stmt, 1);
json_object_set_new(json_obj, "id", json_integer(id));
json_object_set_new(json_obj, "value", json_string((const char *)value));
json_array_append_new(json_arr, json_obj);
}
char *json_str = json_dumps(json_arr, JSON_INDENT(2));
response = strdup(json_str);
// Cleanup
free(json_str);
json_decref(json_arr);
sqlite3_finalize(stmt);
return response;
}
int db_insert(const char *value) {
const char *sql = "INSERT INTO data (value) VALUES (?);";
sqlite3_stmt *stmt;
int rc;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
return 1;
}
// Bind the value parameter
sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT);
// Execute the statement
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
return 1;
}
return 0;
}
void db_close() {
sqlite3_close(db);
}

112
apiv2/src/main.c Normal file
View File

@@ -0,0 +1,112 @@
#include "mongoose.h"
#include "database.h"
#include <stdlib.h>
#include <stdio.h>
#include <jansson.h>
static const char *s_http_port = "127.0.0.1:8001";
static const char *s_root_dir = "."; // Serve current directory
static void handle_get(struct mg_connection *c, struct mg_http_message *hm) {
char query[256] = {0};
// Parse query parameter 'q'
mg_http_get_var(&hm->query, "q", query, sizeof(query));
char *response = db_query(query);
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s", response);
free(response);
}
static void handle_post(struct mg_connection *c, struct mg_http_message *hm) {
char *response;
int result;
// Get the JSON data from the body
char *body = malloc(hm->body.len + 1);
memcpy(body, hm->body.buf, hm->body.len);
body[hm->body.len] = '\0';
// Parse JSON data
json_error_t error;
json_t *root = json_loads(body, 0, &error);
free(body);
if (!root) {
mg_http_reply(c, 400, "Content-Type: text/plain\r\n", "Invalid JSON data: %s", error.text);
return;
}
// Extract data from JSON
const char *value = json_string_value(json_object_get(root, "value"));
if (!value) {
mg_http_reply(c, 400, "Content-Type: text/plain\r\n", "Missing 'value' in JSON data");
json_decref(root);
return;
}
// Store data in the database
result = db_insert(value);
if (result == 0) {
mg_http_reply(c, 200, "Content-Type: text/plain\r\n", "Data stored successfully");
} else {
mg_http_reply(c, 500, "Content-Type: text/plain\r\n", "Failed to store data");
}
json_decref(root);
}
static void handle_api_call(struct mg_connection *c, struct mg_http_message *hm) {
if (mg_strcasecmp(hm->method, mg_str("GET")) == 0) {
handle_get(c, hm);
} else if (mg_strcasecmp(hm->method, mg_str("POST")) == 0) {
handle_post(c, hm);
} else {
mg_http_reply(c, 405, "Content-Type: text/plain\r\n", "Method Not Allowed");
}
}
static void fn(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
if (mg_match(hm->uri, mg_str("/api"), NULL)) {
handle_api_call(c, hm);
} else {
struct mg_http_serve_opts opts = {.root_dir = s_root_dir};
mg_http_serve_dir(c, hm, &opts);
}
}
}
int main(void) {
struct mg_mgr mgr;
mg_mgr_init(&mgr);
// Initialize the database
if (db_init() != 0) {
printf("Failed to initialize database\n");
return 1;
}
printf("Starting RESTful API on port %s\n", s_http_port);
// Start listening for connections
if (mg_http_listen(&mgr, s_http_port, fn, NULL) == NULL) {
printf("Failed to create listener\n");
return 1;
}
for (;;) {
mg_mgr_poll(&mgr, 1000);
}
mg_mgr_free(&mgr);
db_close();
return 0;
}

1
apiv2g/go.mod Normal file
View File

@@ -0,0 +1 @@
module apiv2g

25
apiv2g/main.go Normal file
View File

@@ -0,0 +1,25 @@
package main
import (
"fmt"
)
//TIP To run your code, right-click the code and select <b>Run</b>. Alternatively, click
// the <icon src="AllIcons.Actions.Execute"/> icon in the gutter and select the <b>Run</b> menu item from here.
func main() {
//TIP Press <shortcut actionId="ShowIntentionActions"/> when your caret is at the underlined or highlighted text
// to see how GoLand suggests fixing it.
s := "gopher"
fmt.Println("Hello and welcome, %s!", s)
for i := 1; i <= 5; i++ {
//TIP You can try debugging your code. We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
// for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>. To start your debugging session,
// right-click your code in the editor and select the <b>Debug</b> option.
fmt.Println("i =", 100/i)
}
}
//TIP See GoLand help at <a href="https://www.jetbrains.com/help/go/">jetbrains.com/help/go/</a>.
// Also, you can try interactive lessons for GoLand by selecting 'Help | Learn IDE Features' from the main menu.

1220
apiv2r/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

29
apiv2r/Cargo.toml Normal file
View File

@@ -0,0 +1,29 @@
[package]
name = "zoblak"
version = "2.0.0"
authors = ["Senad Uka <senad@uka.life>"]
edition = "2024"
[dependencies]
actix-web = "4.0"
actix-files = "0.6"
rusqlite = "0.29.0"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
[dev-dependencies]
quickcheck = "0.9"
speculate = "0.1"
parking_lot = { version = "0.12", features = ["nightly"] }
[dependencies.rocket_contrib]
version = "0.4"
default-features = false
features = ["json", "diesel_postgres_pool"]
[features]
default = [edition2024]

View File

@@ -30,3 +30,5 @@ standard-minifier-css
standard-minifier-js
iron:router
accolver:twilio-meteor
chart:chart
reactive-var

View File

@@ -15,6 +15,7 @@ boilerplate-generator@1.0.7
caching-compiler@1.0.3
caching-html-compiler@1.0.5
callback-hook@1.0.7
chart:chart@1.0.1-beta.4
check@1.1.3
coffeescript@1.0.16
ddp@1.2.4

View File

@@ -7,10 +7,28 @@
<strong class="bg-danger">{{ pretty_reasons alarmReasons }}</strong>
<img src="/images/alarm.gif" class="img-responsive center-block" id="alarm_image" />
<button id="stop_alarm" class="btn btn-danger"> <i class="fa fa-ban"></i> Prekini </button>
{{/if}}
{{#if alarmStopped}}
<strong class="bg-danger">{{ pretty_reasons alarmReasons }}</strong>
<br /><br />
<div class="bg-warning"> <strong>Alarm prekinut!</strong> Nadzor će se nastaviti kada se temperatura vrati u zadani raspon.
</div>
<br /><br />
{{/if}}
{{/with}}
{{#with last_reading}}
<div class="huge_text"> {{ all_temperatures }}</div>
<div class="bigger_text centeredtable">
<table>
{{#each temperature in all_temperatures }}
<tr>
<td>{{temperature.name}}:</td>
<td> {{temperature.value}}</td>
</tr>
{{/each}}
</table>
</div>
<div>{{pretty_time created_at}}</div>
{{/with}}
<button id="run_alarm_settings" class="btn btn-default"> <i class="fa fa-wrench"></i> Podešavanje </button>

View File

@@ -1,30 +1,7 @@
function sensor_data_collection() {
var controllerId = Session.get('controller_id');
return SensorData.find({
controllerId: controllerId
}, {
sort: {
created_at: -1
},
limit: 3
});
}
function last_sensor_reading() {
var controller = Session.get('controller_id');
var result = null;
if (controller) {
result = sensor_data_collection();
}
if (result && result.count() > 0) {
return result.fetch()[0];
} else {
return {}
}
}
Template.alarm.helpers({
last_reading: last_sensor_reading,
last_reading: Meteor.zoblak.client.last_sensor_reading,
state: function() {
return Meteor.zoblak.client.controller_state().state;
},
@@ -32,11 +9,15 @@ Template.alarm.helpers({
return moment(time).format("DD.MM.YYYY, HH:mm")
},
all_temperatures: function() {
var result = "";
var temperatures = last_sensor_reading().temperatures;
var result = [];
var names = Meteor.zoblak.client.controller_state().config['sensorNames'] || {};
var temperatures = Meteor.zoblak.client.last_sensor_reading().temperatures;
for (var i in temperatures) {
result += '' + parseFloat(temperatures[i]).toFixed(1) + ' °C ';
var temperature = parseFloat(temperatures[i]).toFixed(1);
var temperatureLabel = (Meteor.zoblak.shared.valid_temperature(temperature)) ? temperature : "XX.X";
var name = names[i] || "Senzor " + i.toString();
result.push({ name: name, value: temperatureLabel + ' °C ' });
}
return result;
},

View File

@@ -52,28 +52,55 @@
<td>2.
</td>
<td>
<input required name="sms2" type="tel" id="sms2" placeholder="+3876xxxxxxxx" value={{ config 'sms2'}}/>
<input required name="sms2" type="tel" id="sms2" placeholder="+3876xxxxxxxx" value={{ config 'sms2'}} />
</td>
</tr>
<tr>
<td>3.
</td>
<td>
<input required name="sms3" type="tel" id="sms3" placeholder="+3876xxxxxxxx" value={{ config 'sms3'}}/>
<input required name="sms3" type="tel" id="sms3" placeholder="+3876xxxxxxxx" value={{ config 'sms3'}} />
</td>
</tr>
<tr>
<td>4.
</td>
<td>
<input required name="sms4" type="tel" id="sms4" placeholder="+3876xxxxxxxx" value={{ config 'sms4'}}/>
<input required name="sms4" type="tel" id="sms4" placeholder="+3876xxxxxxxx" value={{ config 'sms4'}} />
</td>
</tr>
</table>
</div>
<div class="form-group">
<h3>Senzori: </h3>
<table class="table">
{{#each sensor in sensors }}
<tr>
<td>
{{ pretty_temperature sensor.value }}
</td>
<td>
<label class="switch">
<input type="checkbox" checked={{sensor.on}} class="sensor_switch" />
<div class="slider round"></div>
</label>
</td>
<td>
<label>
<input type="text" placeholder="prostorija" value={{sensor.name}} class="sensor_name" maxlength="7" />
</label>
</td>
</tr>
{{/each}}
</table>
</div>
</div>
<div class="modal-footer">
<button id="cancel_settings" class="btn btn-danger" name="cancel_settings" data-dismiss="modal">Odgodi</button>
<button id="save_settings" class="btn btn-default" name="save_settings" data-dismiss="modal">Zapamti</button>
</div>

View File

@@ -26,12 +26,36 @@ Template.alarm_settings.helpers({
var result = config()[property];
console.log('returning', result);
return result;
},
pretty_temperature: function(temperature) {
var temperatureLabel = (Meteor.zoblak.shared.valid_temperature(temperature)) ? temperature : "XX.X";
return '' + temperatureLabel + ' °C ';
},
sensors: function() {
var temperatures = Meteor.zoblak.client.last_sensor_reading().temperatures;
var enabled = config()['sensorsEnabled'] || {};
var sensorNames = config()['sensorNames'] || {};
var sensors = [];
for (var index in temperatures) {
var is_on = (index in enabled) ? enabled[index] : true; // on by default
var name = (index in sensorNames) ? sensorNames[index] : null; // no name by default
var value = parseFloat(temperatures[index])
sensors.push({
value: value,
on: is_on,
name: name
})
}
return sensors;
}
});
Template.alarm_settings.events({
'click #save_settings': function() {
var controller_id = Meteor.zoblak.client.controller_state().controller_id;
1
var instance = Template.instance();
var minTemperature = instance.$('#min_temperature').val();
var maxTemperature = instance.$('#max_temperature').val();
@@ -42,6 +66,25 @@ Template.alarm_settings.events({
var sms3 = instance.$('#sms3').val();
var sms4 = instance.$('#sms4').val();
Meteor.call('saveAlarmSettings', controller_id, minTemperature, maxTemperature, timeoutBox, timeoutPhone, [sms1, sms2, sms3, sms4]);
var sensorSwitches = instance.$('.sensor_switch');
var enabled = {};
sensorSwitches.each( function(index,element) {
enabled[index] = instance.$(element).is(':checked');
} );
console.log("Enabled: ", enabled);
var names = {};
var sensorNames = instance.$('.sensor_name');
sensorNames.each( function(index,element) {
names[index] = instance.$(element).val();
} );
console.log("Names ", names);
Meteor.call('saveAlarmSettings', controller_id, minTemperature, maxTemperature, timeoutBox, timeoutPhone, [sms1, sms2, sms3, sms4] ,enabled, names);
}
});

View File

@@ -1,5 +1,5 @@
// at the beginning
Session.set("templateName", "start");
Session.set("templateName", "alarm");
Template.body.helpers({
template_name: function() {

View File

@@ -18,6 +18,27 @@
font-size: 2.5em;
}
.bigger_text {
font-size: 1.8em;
}
div.centeredtable
{
text-align: center;
}
div.centeredtable table
{
border-collapse: separate;
margin: 0 auto;
text-align: left;
border-spacing: 10px;
}
div.centeredtable table, div.centeredtable td{
border: 1px solid #eeeeee;
}
@media all and (orientation: portrait) {
#bucket_image {
width: 90%;
@@ -58,3 +79,70 @@
font-size: 14px;
}
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
/* Hide default HTML checkbox */
.switch input {display:none;}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
#chartHorizontalScroll {
width: 100%;
overflow-x: scroll;
overflow-y: hidden;
white-space:nowrap;
}

View File

@@ -1,12 +1,16 @@
<template name="log">
<div id="chartHorizontalScroll">
<canvas id="temperatureChart"></canvas>
</div>
<!--
<div class="hello">
<ul class="no-bullets">
{{#each sensorDataCollection}}
{{> sensorData}}
{{/each}}
</ul>
</div>
-->
</template>

View File

@@ -4,7 +4,7 @@ function sensor_data_collection() {
controllerId: controllerId
}, {
sort: {
created_at: -1
created_at: 1
},
limit: 100
});
@@ -20,10 +20,194 @@ Template.log.events({
}
});
var tempHolder = {};
var createChart = function() {
var self = tempHolder;
self.labels = [];
self.temperatures = [];
Tracker.autorun(function() {
var sdc = sensor_data_collection();
sdc.forEach(function(sensorReading) {
if (sensorReading.temperatures) {
self.labels.push(moment(sensorReading.lastBoxContact).format('ddd DD.MM. HH:mm:ss'));
for (var i = 0; i < sensorReading.temperatures.length; i++) {
if (!self.temperatures[i]) {
self.temperatures[i] = [];
}
self.temperatures[i].push(sensorReading.temperatures[i]);
}
}
});
});
if(self.temperatures.length <= 0) { return; } /// there are no temperatures
// Get the context of the canvas element we want to select
var chartCanvas = document.getElementById("temperatureChart");
var lengthOfTemperatures = self.temperatures[0].length * 40 + 10;
chartCanvas.width = (window.innerWidth > lengthOfTemperatures) ? window.innerWidth : lengthOfTemperatures;
var heightOfElementsBefore = chartCanvas.getBoundingClientRect().top;
chartCanvas.height = window.innerHeight - heightOfElementsBefore;
var ctx = chartCanvas.getContext("2d");
// Set the options
var options = {
///Boolean - Whether grid lines are shown across the chart
scaleShowGridLines: true,
//String - Colour of the grid lines
scaleGridLineColor: "rgba(0,0,0,.05)",
//Number - Width of the grid lines
scaleGridLineWidth: 1,
//Boolean - Whether to show horizontal lines (except X axis)
scaleShowHorizontalLines: true,
//Boolean - Whether to show vertical lines (except Y axis)
scaleShowVerticalLines: true,
//Boolean - Whether the line is curved between points
bezierCurve: true,
//Number - Tension of the bezier curve between points
bezierCurveTension: 0.4,
//Boolean - Whether to show a dot for each point
pointDot: true,
//Number - Radius of each point dot in pixels
pointDotRadius: 4,
//Number - Pixel width of point dot stroke
pointDotStrokeWidth: 1,
//Number - amount extra to add to the radius to cater for hit detection outside the drawn point
pointHitDetectionRadius: 20,
//Boolean - Whether to show a stroke for datasets
datasetStroke: true,
//Number - Pixel width of dataset stroke
datasetStrokeWidth: 2,
//Boolean - Whether to fill the dataset with a colour
datasetFill: true,
//String - A legend template
legendTemplate: "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%> aaa </ul>"
};
var random = function() {
return Math.random() * (100 - 1) + 1;
}
var colours = [{ // blue
fillColor: "rgba(151,187,205,0.2)",
strokeColor: "rgba(151,187,205,1)",
pointColor: "rgba(151,187,205,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(151,187,205,0.8)"
}, { // red
fillColor: "rgba(247,70,74,0.2)",
strokeColor: "rgba(247,70,74,1)",
pointColor: "rgba(247,70,74,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(247,70,74,0.8)"
}, { // green
fillColor: "rgba(70,191,189,0.2)",
strokeColor: "rgba(70,191,189,1)",
pointColor: "rgba(70,191,189,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(70,191,189,0.8)"
}, { // light grey
fillColor: "rgba(220,220,220,0.2)",
strokeColor: "rgba(220,220,220,1)",
pointColor: "rgba(220,220,220,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(220,220,220,0.8)"
},
{ // yellow
fillColor: "rgba(253,180,92,0.2)",
strokeColor: "rgba(253,180,92,1)",
pointColor: "rgba(253,180,92,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(253,180,92,0.8)"
}, { // grey
fillColor: "rgba(148,159,177,0.2)",
strokeColor: "rgba(148,159,177,1)",
pointColor: "rgba(148,159,177,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(148,159,177,0.8)"
}, { // dark grey
fillColor: "rgba(77,83,96,0.2)",
strokeColor: "rgba(77,83,96,1)",
pointColor: "rgba(77,83,96,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(77,83,96,1)"
}
];
var datasets = [];
for (var i = 0; i < self.temperatures.length; i++) {
// repeat colors once you run out
var colour = colours[i % colours.length];
var dataset = Object.assign({
label: "Senzor #",
data: self.temperatures[i]
}, colour);
datasets.push(dataset);
}
// Set the data
var data = {
labels: self.labels,
datasets: datasets
};
// draw the charts
var myLineChart = new Chart(ctx).Line(data, options);
};
Template.log.onRendered(function() {
createChart();
});
Template.log.resized = function() {
createChart();
return Session.get('resize');
}
Template.log.orientation = function() {
createChart();
return Session.get('orientation');
}
Template.sensorData.helpers({
created_at_formatted: function() {
return moment(this.created_at).format(/*"DD.MM.YYYY, */ " (HH:mm)")
return moment(this.created_at).format( /*"DD.MM.YYYY, */ " (HH:mm)")
},
all_temperatures: function(temperatures) {
var result = '';
if (temperatures.length > 0) {

View File

@@ -1,34 +1,16 @@
Tracker.autorun(function() {
var id = Session.get('controller_id');
Session.set("orientation", new Date());
Session.set("resize", new Date());
if (id) {
Meteor.subscribe("sensor_data", id);
Meteor.subscribe("controller_state", id);
Meteor.subscribe('pictures', id);
}
window.addEventListener('orientationchange', function(){
Session.set("orientation", new Date());
});
window.addEventListener('resize', function(){
Session.set("resize", new Date());
});
});
function safeRoute(route) {
return function () {
var controllerId = this.params.query.controller_id;
if(controllerId) {
Session.setPersistent('controller_id', controllerId);
Session.setPersistent('hide_controller_selection', true);
} else {
Session.setPersistent('hide_controller_selection', false);
}
console.log('go ', route);
if (Meteor.zoblak.client.accessible(route)) {
Session.set('templateName', route);
} else {
Session.set('templateName', 'no_access')
}
}
}
Router.route('/', safeRoute('start'));
Router.route('/alarm', safeRoute('alarm'));
Router.route('/log', safeRoute('log'));
Router.route('/surveillance', safeRoute('surveillance'));
Router.route('/weather', safeRoute('weather'));

View File

@@ -10,7 +10,7 @@
{{/if}}
{{#if accessible 'log'}}
<li role="presentation" class="{{ class_for 'log' }}"><a class="clickable">Danas</a></li>
<li role="presentation" class="{{ class_for 'log' }}"><a class="clickable">Pregled</a></li>
{{/if}}
{{#if accessible 'surveillance'}}

View File

@@ -34,7 +34,7 @@ function saveParamsAndGo(where) {
Template.tabs.events({
'click .start': function() {
saveParamsAndGo('/');
saveParamsAndGo('/water');
},
'click .weather': function() {
saveParamsAndGo('/weather');

View File

@@ -24,11 +24,43 @@ Meteor.zoblak.client = {
if (!controller.features) return false;
return controller.features[feature] === true;
},
sensor_data_collection: function() {
var controllerId = Session.get('controller_id');
return SensorData.find({
controllerId: controllerId
}, {
sort: {
created_at: -1
},
limit: 3
});
},
last_sensor_reading: function() {
var controller = Session.get('controller_id');
var result = null;
if (controller) {
result = Meteor.zoblak.client.sensor_data_collection();
}
if (result && result.count() > 0) {
return result.fetch()[0];
} else {
return {}
}
}
}
Meteor.zoblak.shared = {
valid_temperature: function(value) {
return (parseFloat(value) > -40 && parseFloat(value) < 50);
}
}
Meteor.zoblak.server = {
controller_state: function(controller_id) {
var result = {}
if (controller_id) {
result = ControllerState.findOne({
@@ -36,9 +68,30 @@ Meteor.zoblak.server = {
});
}
console.log("Asked for ", controller_id, " got ", result);
if (!result) {
result = {}
};
return result;
},
on_all_controllers: function(whatToDo) {
var ids = ControllerState.find({}, {
fields: {
'controller_id': 1
}
}).map(function(cid) {
return cid.controller_id
});
for (var index in ids) {
var controller_id = ids[index];
try {
whatToDo(controller_id);
} catch (err) {
console.log('Cannot call ', whatToDo, controller_id, err);
}
}
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say voice="woman" language="english">Zoblak Farm ALARM! ALARM! ALARM!</Say>
</Response>

26
app/router.js Normal file
View File

@@ -0,0 +1,26 @@
function safeRoute(route) {
return function () {
var controllerId = this.params.query.controller_id;
if(controllerId) {
Session.setPersistent('controller_id', controllerId);
Session.setPersistent('hide_controller_selection', true);
} else {
Session.setPersistent('hide_controller_selection', false);
}
console.log('go ', route);
if (Meteor.zoblak.client.accessible(route)) {
Session.set('templateName', route);
} else {
Session.set('templateName', 'no_access')
}
}
}
Router.route('/', safeRoute('alarm'));
Router.route('/water', safeRoute('start'));
Router.route('/alarm', safeRoute('alarm'));
Router.route('/log', safeRoute('log'));
Router.route('/surveillance', safeRoute('surveillance'));
Router.route('/weather', safeRoute('weather'));

View File

@@ -9,6 +9,7 @@ Api.addRoute('sensorData', {
authRequired: false
}, {
post: function() {
console.log("Sensordata ", this.bodyParams);
reactToSensorData(this.bodyParams);
var sensorObject = {
temperatureValue: parseFloat(this.bodyParams.temperatureValue),
@@ -63,8 +64,10 @@ Api.addRoute('alarm/:id/phonePing', {
reactToSensorData = function(nextSensorReading) {
console.log("reacting to sensor");
var controllerId = nextSensorReading.controllerId;
console.log("reacting to sensor ", nextSensorReading);
var stateObject = stateOrDefault(controllerId);
var state = stateObject.state;
var config = stateObject.config;
@@ -201,8 +204,8 @@ function stateOrDefault(id) {
manualInflow: true
},
features: {
start: true,
weather: true,
start: false,
weather: false,
surveillance: true,
log: true,
alarm: true

View File

@@ -143,7 +143,7 @@ function saveControllerConfig(controller_id, time, days, manualInflow) {
});
}
function saveAlarmSettings(controller_id, minTemperature, maxTemperature, timeoutBox, timeoutPhone, smsNumbers) {
function saveAlarmSettings(controller_id, minTemperature, maxTemperature, timeoutBox, timeoutPhone, smsNumbers, sensorsEnabled, sensorNames) {
var state = Meteor.zoblak.server.controller_state(controller_id);
ControllerState.update(state._id, {
'$set': {
@@ -155,9 +155,12 @@ function saveAlarmSettings(controller_id, minTemperature, maxTemperature, timeou
'config.sms1': smsNumbers[0],
'config.sms2': smsNumbers[1],
'config.sms3': smsNumbers[2],
'config.sms4': smsNumbers[3]
'config.sms4': smsNumbers[3],
'config.sensorsEnabled': sensorsEnabled,
'config.sensorNames': sensorNames
}
});
var jobName = "automatic_alarm_" + controller_id;
SyncedCron.remove(jobName);
@@ -185,6 +188,7 @@ reactToAlarmData = function(controller_id) {
var state = Meteor.zoblak.server.controller_state(controller_id);
var config = state.config;
var minTemperature = function(temperatures) {
// if it gets a lot colder than absolute zero
// we will have more problems than the bug in this code
@@ -196,7 +200,7 @@ reactToAlarmData = function(controller_id) {
}
}
return minimal;
}
};
var maxTemperature = function(temperatures) {
// obviously - hell is not supported in this version
@@ -208,17 +212,24 @@ reactToAlarmData = function(controller_id) {
}
}
return maximal;
}
};
var tooCold = config.minTemperature && (minTemperature(reading.temperatures) < config.minTemperature);
var temperatures = (reading.temperatures || []).filter(function(temperature, index) {
var is_on = (index in config.sensorsEnabled)?config.sensorsEnabled[index]: true;
return Meteor.zoblak.shared.valid_temperature(temperature) && is_on;
});
var tooHot = config.maxTemperature && (maxTemperature(reading.temperatures) > config.maxTemperature);
console.log("Konfiguracija: ", controller_id, config);
var tooCold = config.minTemperature && (minTemperature(temperatures) < config.minTemperature);
var tooHot = config.maxTemperature && (maxTemperature(temperatures) > config.maxTemperature);
var minutesSinceLastBoxContact = reading.lastBoxContact ? moment(new Date()).diff(moment(reading.lastBoxContact), 'minutes') : -1;
var boxSilent = config.timeoutBox && minutesSinceLastBoxContact > config.timeoutBox;
var minutesSinceLastPhoneContact = state.lastPhoneContact ? moment(new Date()).diff(moment(state.lastPhoneContact), 'minutes') : -1;
var phoneSilent = false;//config.timeoutPhone && minutesSinceLastPhoneContact > config.timeoutPhone;
var phoneSilent = false; //config.timeoutPhone && minutesSinceLastPhoneContact > config.timeoutPhone;
console.log("too ", tooCold, tooHot, boxSilent, phoneSilent);
console.log("lpc", state.lastPhoneContact);
@@ -254,7 +265,9 @@ function soundTheAlarm(controller_id, tooCold, tooHot, boxSilent, phoneSilent) {
var smsSent = !!state.state.alarmSmsSent;
var needsToSendSms = !smsSent // && phoneSilent;
var sendSmsPart = needsToSendSms ? { 'state.alarmSmsSent': true } : {};
var sendSmsPart = needsToSendSms ? {
'state.alarmSmsSent': true
} : {};
ControllerState.update(state._id, {
'$set': Object.assign({
@@ -265,17 +278,18 @@ function soundTheAlarm(controller_id, tooCold, tooHot, boxSilent, phoneSilent) {
});
if (needsToSendSms) {
sendAlarmingSms(controller_id,reason, state.config.smsNumbers)
sendAlarmingSms(controller_id, reason, state.config.smsNumbers);
callTheUser(controller_id, reason, state.config.smsNumbers);
}
}
function sendAlarmingSms(controller_id,reason, numbers) {
function sendAlarmingSms(controller_id, reason, numbers) {
for (var i in numbers) {
var number = numbers[i];
twilio = Twilio('AC10d7ed0bf54c1be4b1cd7133130e63f4', 'e133d3f02a69b79e93ad9ca1d73517d1');
twilio.sendSms({
to: number, // Any number Twilio can deliver to
from: '+447481345235', // A number you bought from Twilio and can use for outbound communication
from: '+19282124174', // A number you bought from Twilio and can use for outbound communication
body: 'Zoblak alarm! Pokrenite aplikaciju! HITNO! http://agrar.zoblak.com/alarm?controller_id=' + controller_id // body of the SMS message
}, function(err, responseData) { //this function is executed when a response is received from Twilio
if (!err) { // "err" is an error received during the request, if any
@@ -289,6 +303,21 @@ function sendAlarmingSms(controller_id,reason, numbers) {
}
}
function callTheUser(controller_id, reason, numbers) {
for (var i in numbers) {
var number = numbers[i];
twilio = Twilio('AC10d7ed0bf54c1be4b1cd7133130e63f4', 'e133d3f02a69b79e93ad9ca1d73517d1');
twilio.makeCall({
to: number, // Any number Twilio can call
from: '+441143031932', // A number you bought from Twilio and can use for outbound communication
url: 'https://handler.twilio.com/twiml/EH9491c24474db07ec52b598baa5724f1e' // A URL that produces an XML document (TwiML) which contains instructions for the call
}, function(err, responseData) {
//executed when the call has been initiated.
console.log(err); // outputs "+14506667788"
});
}
}
function stopTheAlarm(controller_id, everythingIsBackToNormal = false) {
// time of alarm stopped is reset so that scheduled job can raise the alarm
// again

View File

@@ -2,6 +2,22 @@ if (Meteor.isServer) {
Meteor.startup(function() {
// code to run on server at startup
SyncedCron.start();
Meteor.zoblak.server.on_all_controllers(function(controller_id) {
if(!controller_id) { return; }; // protects from null controller_id
var jobName = "automatic_alarm_" + controller_id;
SyncedCron.remove(jobName);
SyncedCron.add({
name: jobName,
schedule: function(parser) {
return parser.text('every 10 seconds');
},
job: function() {
reactToAlarmData(controller_id);
}
});
});
});
@@ -23,7 +39,7 @@ if (Meteor.isServer) {
tankLevel2: this.bodyParams.tankLevel2,
tankLevel3: this.bodyParams.tankLevel3,
tankLevel4: this.bodyParams.tankLevel4,
tankFull: this.bodyParams.tankFull,
tankFull: this.bodyParams.tankFull,
startPumpingAt: this.bodyParams.startPumpingAt,
stopPumpingAt: this.bodyParams.stopPumpingAt,
owner: this.bodyParams.owner,

View File

@@ -2,7 +2,15 @@
## Installation
0.
```
mkdir projects
git clone https://github.com/senaduka/tfm
```
1. Go to every subdirectory in drivers directory and follow instructions about installation of drivers
```
/home/pi/projects/tfm/controller/drivers/adafruit# cat README.md
```
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:
@@ -104,3 +112,38 @@ as multiple cameras need to be combined and avconv does not support the appropri
this is being used in /controller/config/__init__.py PICTURE_COMMAND when executing the "convert" command while combining multiple images
```
13. set up fixed IP addresses (rpi3):
```
Edit /etc/dhcpcd.conf as follows:-
Here is an example which configures a static address, routes and dns.
interface eth0
static ip_address=192.168.1.6/24
static routers=192.168.1.1
static domain_name_servers=192.168.1.1
interface wlan0
static ip_address=192.168.1.7/24
static routers=192.168.1.1
static domain_name_servers=192.168.1.1
```
14. set up w1 thermometer sensing
```
DS18B20
W1-thermometer sensing on RPI3
wiring schema: https://cdn-learn.adafruit.com/downloads/pdf/adafruits-raspberry-pi-lesson-11-ds18b20-temperature-sensing.pdf (firrt schema - I used GPIO BCN #17)
edit /boot/config.txt - at the end insert (rpi2 doesn't need the gpiopin, but rpi3 does! see for more info: http://raspberrypi.stackexchange.com/questions/37157/ds18b20-no-longer-working, rpi would need only: dtoverlay=w1-gpio):
dtoverlay=w1-gpio-pullup,gpiopin=17
reboot
issue the following commands, the last directory has a variable name, based on the sensor's serial number!
sudo modprobe w1-gpio
sudo modprobe w1-therm
cd /sys/bus/w1/devices
ls
cd 28-xxxx (change this to match what serial number pops up)
cat w1_slave
```

View File

@@ -50,7 +50,7 @@ try:
except:
print("onewire thermo error:", sys.exc_info()[0])
# Un-comment the line below to convert the temperature to Fahrenheit.
# Un-comment the line below to convert the temperature to Fahrenheiq
# temperature = temperature * 9/5.0 + 32
print 'Temp={0:0.1f}*C'.format(temperature)

171
shelly/syncscript.js Normal file
View File

@@ -0,0 +1,171 @@
const CONTROLLER_ID="550";
const senzori = [
"7c:c6:b6:74:a4:be",
"7c:c6:b6:75:a1:21"
];
const temperature = ["22.0", "22.0"];
const humidity = ["50", "50"];
let ALLTERCO_MFD_ID_STR = "0ba9";
let BTHOME_SVC_ID_STR = "fcd2";
let ALLTERCO_MFD_ID = JSON.parse("0x" + ALLTERCO_MFD_ID_STR);
let BTHOME_SVC_ID = JSON.parse("0x" + BTHOME_SVC_ID_STR);
let SCAN_DURATION = BLE.Scanner.INFINITE_SCAN;
let uint8 = 0;
let int8 = 1;
let uint16 = 2;
let int16 = 3;
let uint24 = 4;
let int24 = 5;
function getByteSize(type) {
if (type === uint8 || type === int8) return 1;
if (type === uint16 || type === int16) return 2;
if (type === uint24 || type === int24) return 3;
//impossible as advertisements are much smaller;
return 255;
}
let BTH = [];
BTH[0x00] = { n: "pid", t: uint8 };
BTH[0x01] = { n: "Battery", t: uint8, u: "%" };
BTH[0x3a] = { n: "Button", t: uint8 };
BTH[0x2e] = { n: "Humidity", t: uint8 };
BTH[0x45] = { n: "Temperature", t: int16, f: 0.1 };
let BTHomeDecoder = {
utoi: function (num, bitsz) {
let mask = 1 << (bitsz - 1);
return num & mask ? num - (1 << bitsz) : num;
},
getUInt8: function (buffer) {
return buffer.at(0);
},
getInt8: function (buffer) {
return this.utoi(this.getUInt8(buffer), 8);
},
getUInt16LE: function (buffer) {
return 0xffff & ((buffer.at(1) << 8) | buffer.at(0));
},
getInt16LE: function (buffer) {
return this.utoi(this.getUInt16LE(buffer), 16);
},
getUInt24LE: function (buffer) {
return (
0x00ffffff & ((buffer.at(2) << 16) | (buffer.at(1) << 8) | buffer.at(0))
);
},
getInt24LE: function (buffer) {
return this.utoi(this.getUInt24LE(buffer), 24);
},
getBufValue: function (type, buffer) {
if (buffer.length < getByteSize(type)) return null;
let res = null;
if (type === uint8) res = this.getUInt8(buffer);
if (type === int8) res = this.getInt8(buffer);
if (type === uint16) res = this.getUInt16LE(buffer);
if (type === int16) res = this.getInt16LE(buffer);
if (type === uint24) res = this.getUInt24LE(buffer);
if (type === int24) res = this.getInt24LE(buffer);
return res;
},
unpack: function (buffer) {
// beacons might not provide BTH service data
if (typeof buffer !== "string" || buffer.length === 0) return null;
let result = {};
let _dib = buffer.at(0);
result["encryption"] = _dib & 0x1 ? true : false;
result["BTHome_version"] = _dib >> 5;
if (result["BTHome_version"] !== 2) return null;
//Can not handle encrypted data
if (result["encryption"]) return result;
buffer = buffer.slice(1);
let _bth;
let _value;
while (buffer.length > 0) {
_bth = BTH[buffer.at(0)];
if (typeof(_bth) === "undefined") {
console.log("BTH: unknown type");
break;
}
buffer = buffer.slice(1);
_value = this.getBufValue(_bth.t, buffer);
if (_value === null) break;
if (typeof _bth.f !== "undefined") _value = _value * _bth.f;
result[_bth.n] = _value;
buffer = buffer.slice(getByteSize(_bth.t));
}
return result;
},
};
let ShellyBLUParser = {
getData: function (res) {
let result = BTHomeDecoder.unpack(res.service_data[BTHOME_SVC_ID_STR]);
result.addr = res.addr;
result.rssi = res.rssi;
return result;
},
};
function sendToZoblak() {
const url = "http://agrar.zoblak.com/api/v1.0/sensorData";
const owner = "Controller: " + CONTROLLER_ID;
const payload = {
owner: owner,
temperatureValue: temperature[0],
humidityValue: humidity[0],
controllerId: CONTROLLER_ID,
temperatures: temperature
};
Shelly.call(
"HTTP.POST", {
"url": url,
"body": JSON.stringify(payload)
},
function(result) {
if (result) {
print("server says: ", result.code);
}
else {
print("There is no server response present. Check connection!")
}
}
);
}
function handleScanResult(event, result) {
if(event === BLE.Scanner.SCAN_RESULT) {
for (var i in senzori) {
if(result.addr === senzori[i]) {
let data = ShellyBLUParser.getData(result);
if(data.Temperature && data.Humidity) {
temperature[i] = data.Temperature.toString();
humidity[i] = data.Humidity.toString();
sendToZoblak();
} else {
console.log("Nesto nije uredu");
}
}
}
}
}
//BLE.Scanner.Subscribe(handleScanResult);
BLE.Scanner.Start({ duration_ms: SCAN_DURATION, active: false }, handleScanResult);