├── .gitignore
├── README.md
├── climaduino
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
├── history
├── __init__.py
├── admin.py
├── graphs.py
├── models.py
├── templates
│ └── history
│ │ └── index.html
├── tests.py
├── urls.py
└── views.py
├── manage.py
├── settings
├── __init__.py
├── admin.py
├── climaduino_controller.py
├── climaduino_programming_sentry.py
├── models.py
├── rrdtool_log.py
├── templates
│ ├── base_template.html
│ └── settings
│ │ ├── device_index.html
│ │ ├── index.html
│ │ ├── individual.html
│ │ └── programs.html
├── tests.py
├── urls.py
└── views.py
└── startup_script for Debian
└── climaduino-controller
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | .DS_Store
3 | *.log
4 | *.logc
5 | *.sublime-project
6 | *.sublime-projectc
7 | *.sublime-workspace
8 | *.sqlite3
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Climaduino Controller
2 | =====================
3 |
4 | The Climaduino Controller is Raspberry Pi-based and interacts with the Arduino-based Climaduino Thermostat to provide a web interface optimized for mobile use. The Web Controller allows parameters on the Thermostat to be tweaked and generates historical graphs of temperature and humidity readings. It also can be used as a programmable thermostat.
5 |
6 | The controller communicates with the thermostat over a serial connection provided over USB. There is a USB cable connecting both the Arduino and the Raspberry Pi. The controller leverages the Django web framework and JQuery Mobile to provide a site optimized for mobile.
7 |
8 | More information
9 | ----------------
10 |
11 | See the instructable with all the information here: http://www.instructables.com/id/Introducing-Climaduino-The-Arduino-Based-Thermosta/
--------------------------------------------------------------------------------
/climaduino/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bbustin/Climaduino-web-controller/33dfb53af45f0cf50d23568b68dbafde86aad006/climaduino/__init__.py
--------------------------------------------------------------------------------
/climaduino/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for climaduino project.
3 |
4 | For more information on this file, see
5 | https://docs.djangoproject.com/en/1.6/topics/settings/
6 |
7 | For the full list of settings and their values, see
8 | https://docs.djangoproject.com/en/1.6/ref/settings/
9 | """
10 |
11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
12 | import os
13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__))
14 | TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')]
15 | # Quick-start development settings - unsuitable for production
16 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/
17 |
18 | # SECURITY WARNING: keep the secret key used in production secret!
19 | SECRET_KEY = 's_&%8+d(b_q57rx94g+pbm)enqn(=%@+i4ow0_ruu2#dp=(q03'
20 |
21 | # SECURITY WARNING: don't run with debug turned on in production!
22 | DEBUG = True
23 |
24 | TEMPLATE_DEBUG = True
25 |
26 | ALLOWED_HOSTS = ['*']
27 |
28 |
29 | # Application definition
30 |
31 | INSTALLED_APPS = (
32 | 'django.contrib.admin',
33 | 'django.contrib.auth',
34 | 'django.contrib.contenttypes',
35 | 'django.contrib.sessions',
36 | 'django.contrib.messages',
37 | 'django.contrib.staticfiles',
38 | 'settings',
39 | 'history',
40 | )
41 |
42 | MIDDLEWARE_CLASSES = (
43 | 'django.contrib.sessions.middleware.SessionMiddleware',
44 | 'django.middleware.common.CommonMiddleware',
45 | 'django.middleware.csrf.CsrfViewMiddleware',
46 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
47 | 'django.contrib.messages.middleware.MessageMiddleware',
48 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
49 | )
50 |
51 | ROOT_URLCONF = 'climaduino.urls'
52 |
53 | WSGI_APPLICATION = 'climaduino.wsgi.application'
54 |
55 |
56 | # Database
57 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases
58 |
59 | DATABASES = {
60 | 'default': {
61 | 'ENGINE': 'django.db.backends.sqlite3',
62 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
63 | }
64 | }
65 |
66 | # Internationalization
67 | # https://docs.djangoproject.com/en/1.6/topics/i18n/
68 |
69 | LANGUAGE_CODE = 'en-us'
70 |
71 | TIME_ZONE = 'EST'
72 |
73 | USE_I18N = True
74 |
75 | USE_L10N = True
76 |
77 | USE_TZ = True
78 |
79 |
80 | # Static files (CSS, JavaScript, Images)
81 | # https://docs.djangoproject.com/en/1.6/howto/static-files/
82 |
83 | STATIC_URL = '/static/'
84 |
--------------------------------------------------------------------------------
/climaduino/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import patterns, include, url
2 | from django.core.urlresolvers import reverse
3 | from django.contrib import admin
4 | admin.autodiscover()
5 |
6 | urlpatterns = patterns('',
7 | # Examples:
8 | # url(r'^$', 'climaduino.views.home', name='home'),
9 | # url(r'^blog/', include('blog.urls')),
10 |
11 | url(r'^$', 'settings.views.index', name="index"), #use the index function from settings for now
12 | url(r'^admin/', include(admin.site.urls)),
13 | url(r'^settings/', include('settings.urls', namespace='settings')),
14 | url(r'^history/', include('history.urls', namespace='history')),
15 | )
16 |
--------------------------------------------------------------------------------
/climaduino/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for climaduino project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "climaduino.settings")
12 |
13 | from django.core.wsgi import get_wsgi_application
14 | application = get_wsgi_application()
15 |
--------------------------------------------------------------------------------
/history/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bbustin/Climaduino-web-controller/33dfb53af45f0cf50d23568b68dbafde86aad006/history/__init__.py
--------------------------------------------------------------------------------
/history/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/history/graphs.py:
--------------------------------------------------------------------------------
1 | graph_type = {}
2 | time_periods = ["15m", "30m", "2h", "1d", "1w", "1m", "1y", "4y"]
3 |
4 | # if cannot import rrdtool, then we will not generate any graphs when create_graphs called
5 | try:
6 | import rrdtool
7 | except:
8 | print("python-rrdtool library is not installed. Graphs will not be available.")
9 |
10 | def create_graphs(device_id):
11 | rrd_file = "temp_humidity-%s.rrd" % device_id
12 |
13 | graph_type = {}
14 | graph_type["temperature"] = {'data_sources': ['DEF:temperatureAvg=%s:temperature:AVERAGE' % rrd_file,
15 | 'DEF:temperatureSet=%s:temperatureSetPoint:AVERAGE' % rrd_file,
16 | #'DEF:temperatureMax=%s:temperature:MAX' % rrd_file,
17 | #'DEF:temperatureMin=%s:temperature:MIN % rrd_file'
18 | ],
19 | 'graph_elements': ['LINE1:temperatureAvg#0000FF:Temp Avg\r',
20 | 'LINE1:temperatureSet#000000:Temp Set\r',
21 | #'LINE1:temperatureMax#00FF00:TemperatureMax\r',
22 | #'LINE1:temperatureMin#FF0000:TemperatureMin\r'
23 | ]}
24 | graph_type["humidity"] = {'data_sources': ['DEF:humidityAvg=%s:humidity:AVERAGE' % rrd_file,
25 | 'DEF:humiditySet=%s:humiditySetPoint:AVERAGE' % rrd_file,
26 | #'DEF:humidityMax=%s:humidity:MAX' % rrd_file,
27 | #'DEF:humidityMin=%s:humidity:MIN' % rrd_file
28 | ],
29 | 'graph_elements': ['LINE1:humidityAvg#0000FF:Hum Avg\r',
30 | 'LINE1:humiditySet#000000:Hum Set\r',
31 | #'LINE1:humidityMax#00FF00:Humidity Max\r',
32 | #'LINE1:humidityMin#FF0000:Humidity Min\r'
33 | ]}
34 |
35 | image_names = [] #keep track of all image paths so we can return it to caller
36 |
37 | for graph in graph_type:
38 | for time_period in time_periods:
39 | image_name = "graph-%s-%s-%s.png" % (device_id, graph, time_period)
40 | image_names.append(image_name)
41 | rrdtool.graph("./history/static/%s" % image_name,
42 | "--start", "-%s" % time_period,
43 | graph_type[graph]['data_sources'],
44 | graph_type[graph]['graph_elements'])
45 | return(image_names)
46 |
47 |
48 |
49 | if __name__ == "__main__":
50 | create_graphs()
--------------------------------------------------------------------------------
/history/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/history/templates/history/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base_template.html" %}
2 | {% block title %}Climaduino History{% endblock %}
3 | {% block header %}
4 | Refresh
5 |
{{device.zonename}} history
6 | Zones
7 | {% endblock %}
8 | {% block content %}
9 |
10 | {% for graph in graphs %}
11 |
12 |
{{ graph }}
13 |
14 |
15 | {% endfor %}
16 |
17 | {% endblock %}
--------------------------------------------------------------------------------
/history/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/history/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import patterns, url
2 | from history import views
3 |
4 | urlpatterns = patterns('',
5 | url(r'^(\d{1,3})$', views.index, name='index'),
6 | )
--------------------------------------------------------------------------------
/history/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 | from settings.models import Device
3 | import graphs
4 |
5 | def index(request, device_id):
6 | device = Device.objects.get(pk=device_id)
7 | # create graphs and get list of file names
8 | file_paths = graphs.create_graphs(device.identifier)
9 | # create a template that shows each graph image
10 | # render the template
11 | return render(request, 'history/index.html',
12 | {'graphs': file_paths,
13 | 'device': device})
14 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "climaduino.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/settings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bbustin/Climaduino-web-controller/33dfb53af45f0cf50d23568b68dbafde86aad006/settings/__init__.py
--------------------------------------------------------------------------------
/settings/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from settings.models import Setting, Reading, Program, Device
3 |
4 | class SettingAdmin(admin.ModelAdmin):
5 | list_filter = ['device', 'time', 'source']
6 | list_display = ['device' ,'time', 'source', 'temperature', 'humidity', 'mode', 'currentlyRunning']
7 | readonly_fields=['currentlyRunning', 'stateChangeAllowed']
8 |
9 | class ReadingAdmin(admin.ModelAdmin):
10 | list_filter = ['device', 'time']
11 | list_display = ('device', 'time', 'temperature', 'humidity')
12 |
13 | class ProgramAdmin(admin.ModelAdmin):
14 | list_filter = ['device', 'mode', 'day', 'time']
15 | list_display = ['device', 'mode', 'day', 'time', 'temperature', 'humidity']
16 |
17 | class DeviceAdmin(admin.ModelAdmin):
18 | list_display = ['zonename', 'name']
19 |
20 | admin.site.register(Setting, SettingAdmin)
21 | admin.site.register(Reading, ReadingAdmin)
22 | admin.site.register(Program, ProgramAdmin)
23 | admin.site.register(Device, DeviceAdmin)
--------------------------------------------------------------------------------
/settings/climaduino_controller.py:
--------------------------------------------------------------------------------
1 | # call from climaduino top-level directory using
2 | # python -m settings.climaduino_controller
3 |
4 | # Used the following for ideas on using coroutines:
5 | # http://www.dabeaz.com/coroutines/index.html
6 | # http://lgiordani.github.io/blog/2013/03/25/python-generators-from-iterators-to-cooperative-multitasking/
7 | import requests, time
8 |
9 | ## Django stuff
10 | import os
11 | os.environ['DJANGO_SETTINGS_MODULE'] = 'climaduino.settings'
12 | import models
13 | from django.utils import timezone
14 | ##
15 | def database_update(device_id, temperature, humidity, tempSetPoint, humiditySetPoint, mode, currentlyRunning, stateChangeAllowed):
16 | update_time = timezone.now()
17 |
18 | # update reading
19 | reading = models.Reading.objects.filter(device__pk=device_id).last()
20 | if not reading:
21 | reading = models.Reading(device_id=device_id, time=update_time, temperature=temperature, humidity=humidity)
22 | else:
23 | reading.time = update_time
24 | reading.temperature = temperature
25 | reading.humidity = humidity
26 | reading.save()
27 |
28 | # update setting
29 | setting = models.Setting.objects.filter(device__pk=device_id).last()
30 | if not setting:
31 | setting = models.Setting(device_id=device_id, time=update_time, source=0, mode=mode, temperature=tempSetPoint, humidity=humiditySetPoint, currentlyRunning=currentlyRunning, stateChangeAllowed=stateChangeAllowed)
32 | else:
33 | setting.time = update_time
34 | setting.source = 0
35 | setting.mode = mode
36 | setting.temperature = tempSetPoint
37 | setting.humidity = humiditySetPoint
38 | setting.currentlyRunning = currentlyRunning
39 | setting.stateChangeAllowed = stateChangeAllowed
40 | setting.save()
41 |
42 | def climaduino_poll(device_name):
43 | url = "http://{}.local/data/get".format(device_name)
44 | try:
45 | request = requests.get(url)
46 | request.raise_for_status()
47 | except (requests.exceptions.HTTPError, requests.exceptions.Timeout, requests.exceptions.ConnectionError) as details:
48 | print("{} - Failed to poll: {}".format(device_name, details))
49 | return(None)
50 | return(request.json()['value'])
51 |
52 | def climaduino_set_parameters(device_name, parameters):
53 | url_base = "http://{}.local/data/put".format(device_name)
54 | for parameter in parameters:
55 | url = "{}/{}/{}".format(url_base, parameter, parameters[parameter])
56 | try:
57 | request = requests.get(url)
58 | request.raise_for_status()
59 | except (requests.exceptions.HTTPError, requests.exceptions.Timeout, requests.exceptions.ConnectionError) as details:
60 | print("{} - Failed to set '{}' parameter: {}".format(device_name, parameter, details))
61 | else:
62 | print("{} - Set '{}' parameter: {}".format(device_name, parameter, parameters[parameter]))
63 |
64 | def main(queue, climaduino_poll_interval_in_seconds):
65 | import Queue
66 | # set process niceness value to lower its priority
67 | os.nice(1)
68 | print("Climaduino Controller started")
69 | # print results from all Climaduinos and update DB
70 | last_poll = time.time()
71 | while 1:
72 | # get data to set
73 | items_available = True
74 | data_item={}
75 | while items_available:
76 | try:
77 | item = queue.get(False) #non-blocking read
78 | except Queue.Empty:
79 | items_available = False
80 | else:
81 | data_item.update(item)
82 | if len(data_item)>0:
83 | device = models.Device.objects.get(pk=data_item['device_id'])
84 | climaduino_set_parameters(device.name, {'tempSetPoint': data_item['parameters']['temp'],
85 | 'humiditySetPoint': data_item['parameters']['humidity'],
86 | 'mode': data_item['parameters']['mode'],})
87 | if (time.time() - last_poll >= climaduino_poll_interval_in_seconds):
88 | last_poll = time.time()
89 | for device in models.Device.objects.all():
90 | values = climaduino_poll(device.name)
91 | if values:
92 | print(values)
93 | try:
94 | try:
95 | database_update(device.identifier, float(values['temperature']), float(values['humidity']), int(values['tempSetPoint']), int(values['humiditySetPoint']), int(values['mode']), int(values['currentlyRunning']), int(values['stateChangeAllowed']))
96 | except IndexError as details:
97 | print("{} - Unable to update database: {}".format(device.name, details))
98 | # clear queue, otherwise will try to send the setpoints and mode to the Arduino even though that is just where we got the information
99 | items_available = True
100 | while items_available:
101 | try:
102 | queue.get(False) #non-blocking read
103 | except Queue.Empty:
104 | items_available = False
105 | except IndexError as details:
106 | print(details)
107 | time.sleep(.5)
108 |
109 | # print("setting parameters")
110 | # climaduino_set_parameters("climaduinohouse", {'tempSetPoint': 86, 'humiditySetPoint': 58, 'mode': 9})
111 |
112 | # if called directly from the command line, then execute the main() function
113 | if __name__ == "__main__":
114 | print("Must be called from a program.")
115 |
--------------------------------------------------------------------------------
/settings/climaduino_programming_sentry.py:
--------------------------------------------------------------------------------
1 | '''
2 | This code checks whether the Climaduino parameters should be changed based on
3 | day of the week and time of day.
4 |
5 | It will be triggered at regular intervals. The code will not check for the exact time,
6 | but will rather adjust for whatever the interval it is using is. So, if a 5 minute interval
7 | is being used, it is 11:02, and the programming called for the temperature to change at 11,
8 | it will still be changed.
9 |
10 | The code will directly read the database using Django libraries.
11 | '''
12 |
13 | ## Django stuff
14 | import os
15 | os.environ['DJANGO_SETTINGS_MODULE'] = 'climaduino.settings'
16 | import models
17 |
18 | import time, datetime
19 | ##
20 |
21 | def main(interval_in_seconds=300):
22 | # BUG: Does not work when interval wraps around between days. If interval is 5 minutes
23 | # then times between 23:55 and 00:00 (midnight) do not work properly
24 |
25 | # set process niceness value to lower its priority
26 | os.nice(1)
27 | print("Climaduino Programming Sentry Active")
28 | while 1:
29 | print("Programming: Checking")
30 | now = datetime.datetime.now()
31 | current_settings = models.Setting.objects.last()
32 | # find out the day 0 is Monday
33 | current_day = now.weekday()
34 |
35 | # find out the time
36 | current_time = now.time()
37 |
38 | # calculate the time minus interval_in_seconds
39 | earliest_time = now - datetime.timedelta(seconds=interval_in_seconds)
40 | earliest_time = earliest_time.time()
41 |
42 | # query DB with interval_in_seconds "fudge factor"
43 | program_query = models.Program.objects.filter(mode=current_settings.mode, day=current_day, time__range=(earliest_time, current_time))
44 | print(program_query)
45 | # if program exists, find out what should be changed and then change it
46 | for program in program_query:
47 | print("Programming: Setting record")
48 | setting = models.Setting.objects.filter(device__pk=program.device_id).last()
49 | setting.time = now
50 | setting.source = 3
51 | setting.mode = program.mode
52 | setting.temperature = program.temperature
53 | setting.humidity = program.humidity
54 | setting.save()
55 |
56 | # sleep for interval_in_seconds so we only check once during that interval
57 | time.sleep(interval_in_seconds)
58 |
59 | # if called directly from the command line
60 | if __name__ == "__main__":
61 | print("Can not be run directly from the command line.")
--------------------------------------------------------------------------------
/settings/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | import rrdtool_log
3 | import climaduino_programming_sentry
4 | import climaduino_controller
5 |
6 | class Device(models.Model):
7 | identifier = models.IntegerField(primary_key=True)
8 | name = models.CharField("device name - hostname of Arduino Yun", max_length=30)
9 | zonename = models.CharField("zone name", max_length=30)
10 | def __unicode__(self):
11 | return("%s (%d)" % (self.zonename, self.identifier))
12 |
13 | class Setting(models.Model):
14 | device = models.ForeignKey("Device")
15 | time = models.DateTimeField('last change')
16 | source_choices = ((0, 'Climaduino'),(1, 'Controller'), (3, 'Program'))
17 | source = models.IntegerField('source of last change', choices=source_choices, default=0)
18 | mode_choices = ((0, 'Cooling/Humidity Control'), (1, 'Humidity Control'), (5, 'Heating'), (8, 'Fan Only'), (9, 'Off'))
19 | mode = models.IntegerField(choices=mode_choices, default=0)
20 | temperature = models.IntegerField(default=77)
21 | humidity = models.IntegerField(default=55)
22 | currentlyRunning = models.BooleanField(default=False)
23 | stateChangeAllowed = models.BooleanField(default=False)
24 | def __unicode__(self):
25 | return("%s - \n\tmode: %d\n\ttemperature: %d\n\thumidity: %d" % (self.time, self.mode, self.temperature, self.humidity))
26 | def log(self):
27 | queue.put({'device_id': self.device.identifier, 'parameters': {'temp': self.temperature, 'humidity': self.humidity}})
28 | queue_update_parameters.put({'device_id': self.device.identifier, 'parameters': {'temp': self.temperature, 'humidity': self.humidity, 'mode': self.mode}})
29 | # overriding save so we can also log to rrdtool in addition to updating the DB
30 | def save(self, *args, **kwargs):
31 | self.log()
32 | super(Setting, self).save(*args, **kwargs) # save the DB record
33 |
34 | class Reading(models.Model):
35 | device = models.ForeignKey("Device")
36 | time = models.DateTimeField('last change')
37 | temperature = models.DecimalField(max_digits=5, decimal_places=2)
38 | humidity = models.DecimalField(max_digits=5, decimal_places=2)
39 | def __unicode__(self):
40 | return("%s - Readings:\n\ttemperature: %d\n\thumidity: %d" % (self.time, self.temperature, self.humidity))
41 | def log(self):
42 | queue.put({'device_id': self.device.identifier, 'readings': {'temp': self.temperature, 'humidity': self.humidity}})
43 | # overriding save so we can also log to rrdtool in addition to updating the DB
44 | def save(self, *args, **kwargs):
45 | self.log()
46 | super(Reading, self).save(*args, **kwargs) # save the DB record
47 |
48 | class Program(models.Model):
49 | device = models.ForeignKey("Device")
50 | mode_choices = ((0, 'Cooling/Humidity Control'), (1, 'Humidity Control'), (5, 'Heating'), (9, 'Off'))
51 | mode = models.IntegerField(choices=mode_choices, default=0)
52 | time = models.TimeField()
53 | day_choices =((0, "Monday"), (1, "Tuesday"), (2, "Wednesday"), (3, "Thursday"), (4, "Friday"), (5, "Saturday"), (6, "Sunday"))
54 | day = models.IntegerField(choices=day_choices)
55 | temperature = models.IntegerField()
56 | humidity = models.IntegerField()
57 | def __unicode__(self):
58 | return("%s at %s, %s, temperature: %d humidity: %d" % (self.get_day_display(), self.time, self.get_mode_display(), self.temperature, self.humidity))
59 | # prevent creating more than 1 program for any specific day of week/time combination for any device
60 | class Meta:
61 | unique_together = ('device', 'mode', 'day', 'time',)
62 |
63 | # Create a process to log to rrd_tool
64 | import multiprocessing
65 | queue = multiprocessing.Queue()
66 | logger_process = multiprocessing.Process(target=rrdtool_log.main, name="rrdtool logger", args=[queue, 4])
67 | logger_process.daemon = True
68 | logger_process.start()
69 |
70 | queue_update_parameters = multiprocessing.Queue()
71 | controller_process = multiprocessing.Process(target=climaduino_controller.main, name="climaduino controller", args=[queue_update_parameters, 15])
72 | controller_process.daemon = True
73 | controller_process.start()
74 | programming_sentry_process = multiprocessing.Process(target=climaduino_programming_sentry.main, name="programming sentry", args=[60])
75 | programming_sentry_process.daemon = True
76 | programming_sentry_process.start()
77 |
--------------------------------------------------------------------------------
/settings/rrdtool_log.py:
--------------------------------------------------------------------------------
1 | # call from climaduino top-level directory using
2 | # python -m settings.climaduino-controller
3 |
4 | # Used the following for ideas on using coroutines:
5 | # http://www.dabeaz.com/coroutines/index.html
6 | # http://lgiordani.github.io/blog/2013/03/25/python-generators-from-iterators-to-cooperative-multitasking/
7 | try:
8 | import rrdtool
9 | except ImportError:
10 | raise Exception("python-rrdtool library is not installed")
11 | import json, time, os
12 |
13 | def create_database(file_name, interval_in_seconds="60"):
14 | error = rrdtool.create(
15 | file_name, "--step", interval_in_seconds,
16 | "DS:temperature:GAUGE:30:U:U",
17 | "DS:temperatureSetPoint:GAUGE:30:U:U",
18 | "DS:humidity:GAUGE:30:U:U",
19 | "DS:humiditySetPoint:GAUGE:30:U:U",
20 | "RRA:AVERAGE:0.2:1:1440", # 1 day of 1-minute accuracy averages
21 | "RRA:AVERAGE:0.2:5:8640", # 30 days of 5-minute accuracy averages
22 | "RRA:AVERAGE:0.2:15:8640", # 90 days 15 minute averages
23 | "RRA:AVERAGE:0.2:30:8640", # 180 days 30 minute averages
24 | "RRA:AVERAGE:0.2:60:8760", # 1 year 1 hour averages
25 | "RRA:AVERAGE:0.2:1440:1460", # 4 years 1 day averages
26 | "RRA:MAX:0.2:1:1440", # 1 day of 1-minute accuracy maximums
27 | "RRA:MAX:0.2:5:8640", # 30 days of 5-minute accuracy maximums
28 | "RRA:MAX:0.2:15:8640", # 90 days 15 minute maximums
29 | "RRA:MAX:0.2:30:8640", # 180 days 30 minute maximums
30 | "RRA:MAX:0.2:60:8760", # 1 year 1 hour maximums
31 | "RRA:MAX:0.2:1440:1460", # 4 years 1 day maximums
32 | "RRA:MIN:0.2:1:1440", # 1 day of 1-minute accuracy minimums
33 | "RRA:MIN:0.2:5:8640", # 30 days of 5-minute accuracy minimums
34 | "RRA:MIN:0.2:15:8640", # 90 days 15 minute minimums
35 | "RRA:MIN:0.2:30:8640", # 180 days 30 minute minimums
36 | "RRA:MIN:0.2:60:8760", # 1 year 1 hour minimums
37 | "RRA:MIN:0.2:1440:1460", # 4 years 1 day minimums
38 | )
39 | if error:
40 | raise Exception(rrdtool.error())
41 |
42 | def coroutine(func):
43 | '''Convenience decorator to call the first .next() to 'prime' the
44 | co-routine
45 |
46 | taken from one of the sources listed at the top of the page'''
47 |
48 | def start(*args,**kwargs):
49 | cr = func(*args,**kwargs)
50 | cr.next()
51 | return cr
52 | return start
53 |
54 | @coroutine
55 | def log_data():
56 | while 1:
57 | data = (yield)
58 | rrd_file = "temp_humidity-%s.rrd" % data["device_id"]
59 | try:
60 | rrdtool.update(rrd_file, "N:%f:%f:%f:%f" % (data["readings"]["temp"], data["parameters"]["temp"], data["readings"]["humidity"], data["parameters"]["humidity"]))
61 | except rrdtool.error as details:
62 | print(details)
63 | print("Database probably does not exist. Attempting to create it.")
64 | create_database(file_name=rrd_file)
65 | except KeyError as details:
66 | print("%s data was missing. Skipped." % details)
67 |
68 | @coroutine
69 | def display_data():
70 | import pprint
71 | while 1:
72 | data = (yield)
73 | pprint.pprint(data)
74 | # try:
75 | # for key in data:
76 | # print("---- %s ----" % key)
77 | # message = []
78 | # for item in data[key]:
79 | # message.append("%s: %s" % (item, data[key][item]))
80 | # print(", ".join(message))
81 | # except:
82 | # pass
83 |
84 | @coroutine
85 | def broadcast(targets):
86 | while 1:
87 | data = (yield)
88 | for target in targets:
89 | target.send(data)
90 |
91 | def main(queue, interval_in_seconds):
92 | import Queue
93 | # set process niceness value to lower its priority
94 | os.nice(1)
95 |
96 | # comment the line below and uncomment the line below it if you want the data logged to screen
97 | data_logger = broadcast([log_data()]) #broadcast data to logger
98 | #data_logger = broadcast([log_data(), display_data()]) #broadcast data to logger and print to screen
99 |
100 | # We are going to create a loop that looks for a line on Serial. If there is a line,
101 | # send it to the co-routine that interprets and logs it.
102 | #
103 | # If there is a message to send on Serial, it picks it up, and sends it.
104 | print("rrdtool logger started")
105 | while 1:
106 | items_available = True
107 | data_item={}
108 | while items_available:
109 | try:
110 | item = queue.get(False) #non-blocking read
111 | except Queue.Empty:
112 | items_available = False
113 | else:
114 | data_item.update(item)
115 | if len(data_item)>0:
116 | data_logger.send(data_item)
117 | time.sleep(interval_in_seconds)
118 |
119 | # if called directly from the command line, then execute the main() function
120 | if __name__ == "__main__":
121 | main()
122 |
--------------------------------------------------------------------------------
/settings/templates/base_template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% block title %}{% endblock %}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {% block header %}
Climaduino
15 |
Zones
16 | {% endblock %}
17 |
18 |
19 |
20 | {% block content %}{% endblock %}
21 |
22 |
23 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/settings/templates/settings/device_index.html:
--------------------------------------------------------------------------------
1 | {% extends "base_template.html" %}
2 | {% block title %}Climaduino - {{device.zonename}}{% endblock %}
3 | {% block header %}
4 | Refresh
5 | {{device.zonename}}
6 | Zones
7 | {% endblock %}
8 | {% block content %}
9 |
10 |
11 | {% if settings.mode == 0 or settings.mode == 5 %}
12 |
Temperature
13 |
{{ readings.temperature }}º {{ settings.temperature }}º
14 | {% endif %}
15 | {% if settings.mode == 1 or settings.mode == 8 or settings.mode == 9 %}
16 |
Temperature
17 |
{{ readings.temperature }}º -
18 | {% endif %}
19 |
20 |
21 |
22 | {% if settings.mode == 0 or settings.mode == 1 %}
23 |
Humidity
24 |
{{ readings.humidity }}% {{ settings.humidity }}%
25 | {% endif %}
26 | {% if settings.mode == 5 or settings.mode == 8 or settings.mode == 9 %}
27 |
Humidity
28 |
{{ readings.humidity }}% -
29 | {% endif %}
30 |
31 |
32 | {% if settings.currentlyRunning %}System Running{% endif %}
33 |
38 | {% endblock %}
--------------------------------------------------------------------------------
/settings/templates/settings/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base_template.html" %}
2 | {% block title %}Climaduino{% endblock %}
3 | {% block header %}
4 | Zones
5 | {% endblock %}
6 | {% block content %}
7 |
12 | {% endblock %}
13 | {% block footer %}
14 | {% endblock %}
--------------------------------------------------------------------------------
/settings/templates/settings/individual.html:
--------------------------------------------------------------------------------
1 | {% extends "base_template.html" %}
2 | {% block title %}Climaduino Settings - {{device}}{% endblock %}
3 | {% block header %}
4 | Cancel
5 | {{device.name}} {{ action }}
6 | Save
7 | {% endblock %}
8 | {% block content %}
9 |
13 | {% endblock %}
--------------------------------------------------------------------------------
/settings/templates/settings/programs.html:
--------------------------------------------------------------------------------
1 | {% extends "base_template.html" %}
2 | {% block title %}Climaduino Programs{% endblock %}
3 | {% block header %}
4 | {{device.zonename}}
5 | Zones
6 | {% endblock %}
7 | {% block content %}
8 | Not complete. Visit the admin interface for additional functionality such as modifying or deleting programs.
9 |
10 | {% for program in programs %}
11 | {{ program }}
12 | {% endfor %}
13 |
14 |
19 | {% endblock %}
--------------------------------------------------------------------------------
/settings/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/settings/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import patterns, url
2 | from settings import views
3 |
4 | urlpatterns = patterns('',
5 | url(r'^$', views.index, name='index'),
6 | url(r'^(\d{1,3})$', views.device_index, name='device_index'),
7 | url(r'^temperature/(\d{1,3})$', views.set_temperature, name='temperature'),
8 | url(r'^humidity/(\d{1,3})$', views.set_humidity, name='humidity'),
9 | # url(r'^mode$', views.set_mode, name='mode'),
10 | url(r'^programs/(\d{1,3})$', views.programs, name='programs'),
11 | url(r'^climaduino/(\d{1,3})$', views.climaduino, name='climaduino'),
12 | )
--------------------------------------------------------------------------------
/settings/views.py:
--------------------------------------------------------------------------------
1 | import json
2 | from django.http import HttpResponse, HttpResponseRedirect
3 | from django.shortcuts import render
4 | from django.forms import ModelForm
5 | # from django.forms.formsets import formset_factory
6 | from django.utils import timezone
7 | from django.core.urlresolvers import reverse
8 | # from django.views.decorators.http import require_http_methods
9 | from django.views.decorators.csrf import csrf_exempt
10 | from settings.models import Device, Setting, Reading, Program
11 |
12 | class SettingForm(ModelForm):
13 | class Meta:
14 | model = Setting
15 | fields=['mode']
16 |
17 | class TemperatureForm(ModelForm):
18 | class Meta:
19 | model = Setting
20 | fields = ['temperature']
21 |
22 | class HumidityForm(ModelForm):
23 | class Meta:
24 | model = Setting
25 | fields = ['humidity']
26 |
27 | class ProgrammingForm(ModelForm):
28 | class Meta:
29 | model = Program
30 | fields = ['mode', 'day', 'time', 'temperature', 'humidity']
31 |
32 | def device_index(request, device_id):
33 | device = Device.objects.get(pk=device_id)
34 | setting = Setting.objects.filter(device__pk=device_id).last()
35 | if request.method == 'POST':
36 | form = SettingForm(request.POST)
37 | if form.is_valid():
38 | if 'mode' in form.cleaned_data:
39 | setting.mode = form.cleaned_data['mode']
40 | setting.save()
41 | return HttpResponseRedirect(reverse('settings:device_index', args=[device.identifier]))
42 | elif request.method == 'GET':
43 | current_readings = Reading.objects.filter(device__pk=device_id).last()
44 | form = SettingForm(instance=setting)
45 | return render(request, 'settings/device_index.html',
46 | {'form': form,
47 | 'readings': current_readings,
48 | 'settings': setting,
49 | 'device': device})
50 |
51 | def index(request):
52 | devices = Device.objects.all()
53 | if request.method == 'GET':
54 | return render(request, 'settings/index.html',
55 | {'devices': devices})
56 |
57 | def set_temperature(request, device_id):
58 | setting = Setting.objects.filter(device__pk=device_id).last()
59 | device = Device.objects.get(pk=device_id)
60 | if request.method == 'POST':
61 | form = TemperatureForm(request.POST)
62 | if form.is_valid():
63 | if 'temperature' in form.cleaned_data:
64 | setting.temperature = form.cleaned_data['temperature']
65 | setting.save()
66 | return HttpResponseRedirect(reverse('settings:device_index', args=[device.identifier]))
67 | if request.method == 'GET':
68 | return render(request, 'settings/individual.html',
69 | {'action': 'temperature',
70 | 'form_value': setting.temperature,
71 | 'device': device,
72 | 'url_namespace': 'settings:temperature'}
73 | )
74 |
75 | def set_humidity(request, device_id):
76 | setting = Setting.objects.filter(device__pk=device_id).last()
77 | device = Device.objects.get(pk=device_id)
78 | if request.method == 'POST':
79 | form = HumidityForm(request.POST)
80 | if form.is_valid():
81 | if 'humidity' in form.cleaned_data:
82 | setting.humidity = form.cleaned_data['humidity']
83 | setting.save()
84 | return HttpResponseRedirect(reverse('settings:device_index', args=([device.identifier])))
85 | if request.method == 'GET':
86 | return render(request, 'settings/individual.html',
87 | {'action': 'humidity',
88 | 'form_value': setting.humidity,
89 | 'device': device,
90 | 'url_namespace': 'settings:humidity'}
91 | )
92 |
93 | def programs(request, device_id):
94 | program_records = Program.objects.order_by('day', 'time', 'mode')
95 | device = Device.objects.get(pk=device_id)
96 | if request.method == 'POST':
97 | form = ProgrammingForm(request.POST)
98 | if form.is_valid():
99 | program_record = Program(**form.cleaned_data)
100 | program_record.save()
101 | return HttpResponseRedirect(reverse('settings:programs'))
102 | elif request.method == 'GET':
103 | form = ProgrammingForm()
104 | return render(request, 'settings/programs.html',
105 | {'form': form,
106 | 'programs': program_records,
107 | 'device': device}
108 | )
109 |
110 | @csrf_exempt
111 | def climaduino(request, device_id):
112 | if request.method == 'POST':
113 | # if the device record does not exist, create it
114 | try:
115 | device = Device.objects.get(pk=device_id)
116 | except Device.DoesNotExist:
117 | device = Device(identifier=device_id, name="unnamed-%s" % device_id)
118 | device.save()
119 |
120 | update_time = timezone.now()
121 |
122 | setting = Setting.objects.filter(device__pk=device_id).last()
123 | if not setting:
124 | setting = Setting(device_id=device_id, time=update_time, source=1, mode=9, temperature=77, humidity=55)
125 |
126 | # print 'Device ID: %s' % device_id
127 | # print 'Data: %s' % request.body
128 | try:
129 | json_object = json.loads(request.body)
130 | except ValueError:
131 | # print("Not valid JSON")
132 | json_object = None
133 | else:
134 | print('JSON: %s' % json_object)
135 | pass
136 |
137 | response_string = "^" #delimeter to indicate this is where Climaduino should start its parsing
138 | # if we get valid data from the Climaduino
139 | if json_object:
140 | # if we have a previous setting, compare to the data from the Climaduino
141 | ## then determine what to do. Right now, overwrite Climaduino as it is just a remote
142 | ## need way for Climaduino with display to not be overwritten when temp set locally on it
143 | if setting:
144 | if setting.mode != json_object['parameters']['mode']:
145 | response_string = "%s%sM" % (response_string, setting.mode)
146 | if setting.temperature != json_object['parameters']['temp']:
147 | response_string = "%s%sF" % (response_string, setting.temperature)
148 | if setting.humidity != json_object['parameters']['humidity']:
149 | response_string = "%s%s%%" % (response_string, setting.humidity)
150 | # log settings (to rrdtool)
151 | setting.save()
152 |
153 | # update the current readings
154 | reading = Reading.objects.filter(device__pk=device_id).last()
155 | if reading:
156 | reading.time = update_time
157 | reading.temperature = json_object["readings"]["temp"]
158 | reading.humidity = json_object["readings"]["humidity"]
159 | else:
160 | reading = Reading(device_id=device_id, time=update_time, temperature=json_object["readings"]["temp"], humidity=json_object["readings"]["humidity"])
161 | reading.save()
162 |
163 | return(HttpResponse(response_string))
164 |
--------------------------------------------------------------------------------
/startup_script for Debian/climaduino-controller:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | source /home/pi/bin/activate
3 | cd /home/pi/climaduino
4 |
5 | # Carry out specific functions when asked to by the system
6 | case "$1" in
7 | start)
8 | echo "Starting climaduino-controller "
9 | /usr/bin/screen -d -m -S data-logger python manage.py runserver 0.0.0.0:80
10 | ;;
11 | stop)
12 | echo "Stopping climaduino-controller"
13 | echo "oops, can't do that"
14 | ;;
15 | *)
16 | echo "Usage: /etc/init.d/climaduino-controller {start|stop}"
17 | exit 1
18 | ;;
19 | esac
20 |
21 | exit 0
22 |
--------------------------------------------------------------------------------