├── .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 |
24 | {% block footer %} 25 | Settings 26 | Programs 27 | History 28 | {% endblock %} 29 |
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 |
34 | {% csrf_token %} 35 | {{ form.as_p }} 36 | 37 |
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 |
10 | {% csrf_token %} 11 | 12 |
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 | 14 |
15 | {% csrf_token %} 16 | {{ form.as_p }} 17 | 18 |
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 | --------------------------------------------------------------------------------