├── .gitignore ├── .gitmodules ├── INSTALL.md ├── LICENSE ├── METADATA.md ├── README.md ├── UCB-openBAS-intro.docx ├── demo-original ├── app.py ├── static │ ├── bootstrap.min.js │ ├── cal.jpg │ ├── config.json │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.css │ │ ├── bootstrap.css~ │ │ ├── bootstrap.min.css │ │ ├── buttons.css │ │ ├── font-awesome.css │ │ ├── jumbotron-narrow.css │ │ └── slider.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ ├── heater.png │ ├── heater.xcf │ ├── heatlamp.png │ ├── heatlamp.xcf │ ├── jquery.min.js │ ├── js │ │ ├── bootstrap-slider.js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ └── buttons.js │ ├── lamp.png │ ├── light_off.png │ ├── light_on.png │ ├── off.png │ ├── on.png │ ├── pcfan.png │ ├── pcfan.xcf │ ├── scss │ │ ├── buttons.scss │ │ └── partials │ │ │ ├── _buttons.scss │ │ │ ├── _danger.scss │ │ │ ├── _glow.scss │ │ │ └── _options.scss │ ├── sdb.png │ ├── thermo.png │ └── thermo2.png └── templates │ └── index.html ├── install.sh ├── install_virtual.sh ├── introPics.pptx ├── maketarball.sh ├── monitor ├── README.md ├── example.txt ├── report │ ├── app.py │ ├── report.py │ ├── static │ │ ├── debut-light.png │ │ ├── report.css │ │ └── report.js │ └── templates │ │ └── index.html └── rtu_load.py ├── openbas ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── cordova-plugins │ ├── packages │ ├── release │ └── versions ├── LICENSE ├── building.ini ├── client │ ├── actuate.js │ ├── building.html │ ├── building.js │ ├── compatibility │ │ ├── bootstrap-slider.js │ │ └── buttons.js │ ├── css │ │ ├── buttons.css │ │ └── slider.css │ ├── dashboard.css │ ├── dashboard.html │ ├── dashboard.js │ ├── openbas.css │ ├── openbas.html │ ├── openbas.js │ ├── plot.html │ ├── plot.js │ ├── point.css │ ├── point.html │ ├── point.js │ ├── schedule.html │ ├── schedule.js │ ├── status.html │ └── status.js ├── demo.ini ├── followcontroller.py ├── lib │ ├── collections.js │ ├── methods.js │ └── router.js ├── packages │ ├── .gitignore │ ├── anytime │ ├── col-resizable │ ├── jquery-migrate │ ├── jquery-simplecolorpicker │ ├── newd3 │ ├── s3ui │ ├── timezone-js │ └── vakata-jstree ├── private │ ├── rooms.json │ ├── schedules.json │ └── testrooms.json ├── public │ ├── floorplans │ │ └── .DS_Store │ └── img │ │ ├── CIEE-floorplan.png │ │ ├── ajax-loader.gif │ │ └── debut-light.png ├── server │ └── dashboard.js └── settings.json ├── smap-control-example ├── control.ini ├── control.py ├── controllogic.py ├── oat.ini ├── oat.py ├── room.ini ├── room.py └── startdrivers.py ├── smap-control-tutorial ├── control-demo.ini ├── coolControllerService.py ├── oat.ini ├── oat.py ├── pseudoOATdriver.py ├── room.ini ├── roommodeldriver.py ├── schedule-demo.ini ├── schedule.py ├── schedulerService.py ├── schedulerZoneDemo.ini ├── startdrivers.py ├── temperatureDriver.py ├── virtualthermostatDriver.py └── zoneControllerService.py └── zone-controller-tutorial ├── README.md ├── building.ini ├── schedules.json ├── zone1controller.py └── zone2controller.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | monitor/report/report.json 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "upmu-plotter"] 2 | path = upmu-plotter 3 | url = https://github.com/SoftwareDefinedBuildings/upmu-plotter.git 4 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | Installing OpenBAS 2 | =============== 3 | 4 | Welcome to the installation instructions for OpenBAS! This guide is intended to 5 | walk you through setting up your local OpenBAS server. 6 | 7 | ## System Requirements 8 | 9 | OpenBAS requires either Ubuntu Server 14.04 or Ubuntu Desktop 14.04 (Trusty Tahr). We recommend having at least: 10 | 11 | * 1 GHz processor 12 | * 2 GB RAM 13 | * 5 GB disk space 14 | 15 | Any relatively recent consumer-grade machine from the past 5 years should have no problem running Ubuntu and OpenBAS. If you want to do a real deployment, we recommend installing OpenBAS on a dedicated Ubuntu machine. If you are interested in sampling the install process, you can achieve this by using a virtual machine. 16 | 17 | ### Installing Ubuntu 14.04 18 | 19 | Ubuntu 14.04 is available for free download from 20 | [http://www.ubuntu.com/download/desktop/](http://www.ubuntu.com/download/desktop/). 21 | The easiest installation methods are either from DVD or from a USB thumbstick 22 | (instructions for how to do this from Ubuntu, Windows or Mac OS X are all 23 | available [here](http://www.ubuntu.com/download/desktop/)). 24 | 25 | Once you have your DVD or your USB drive with Ubuntu 14.04 loaded, follow the 26 | directions at 27 | [http://www.ubuntu.com/download/desktop/install-ubuntu-desktop](http://www.ubuntu.com/download/desktop/install-ubuntu-desktop) 28 | to install. The instructions are for the Ubuntu Desktop edition, which includes 29 | a graphical interface that is familiar and easier to use for users newer to 30 | Ubuntu. However, because OpenBAS provides a web-based interface, running a 31 | graphical environment on your OpenBAS server is not necessary. The Ubuntu 32 | Server edition will work just as well, if not better, than the Desktop edition. 33 | 34 | **NOTE: do not forget the username and password that you input during the installation process!** 35 | 36 | ### Using a Virtual Machine 37 | 38 | A virtual machine allows you to emulate another operating system on your computer, regardless of which operating system you are running. [VirtualBox](https://www.virtualbox.org/wiki/Downloads) is a free virtual machine. Download the appropriate VirtualBox for your operating system (~128 MB) and download Ubuntu using the links above. When you start up VirtualBox, press "New" to create a new virtual machine, and follow the installation wizard along with the Ubuntu installation instructions (linked above) to create a new Ubuntu installation. 39 | 40 | ## Installing 41 | 42 | 1. **Open the Terminal**: If you are on Ubuntu Desktop, open up the Terminal application by pressing 43 | `Ctl-Alt-T` (pressing the 'Control', 'Alt' and the 't' keys at the same time). 44 | If you are on Ubuntu Server, you should already be looking at a terminal. If 45 | you aren't, turn your computer on. 46 | 47 | **NOTE: when you begin the installation, make sure you are in your home directory. If you are unsure, run the command `cd` in the terminal -- this will place you in your home directory.** 48 | 49 | 2. **Make sure you have `curl` installed**: type the following command into the Terminal and press 'Enter': 50 | 51 | ``` 52 | which curl 53 | ``` 54 | 55 | If this gives you output, like printing out `/usr/bin/curl`, then you are fine! If not, you will need to install 56 | it: 57 | 58 | ``` 59 | sudo apt-get install -y curl 60 | ``` 61 | 62 | You will probably need to type in your password. 63 | 64 | 3. **Install OpenBAS:** We've made this part very easy! Simply type into the terminal the following: 65 | 66 | ``` 67 | curl http://install.openbas.cal-sdb.org/ | sudo bash - 68 | ``` 69 | 70 | which will install and configure your system to run OpenBAS. It will tell you exactly what it is doing, 71 | and by the end of the process, your computer should have OpenBAS installed and running. 72 | 73 | 4. **Configure the sMAP Archiver** (this part will soon be integrated into step 3) 74 | 75 | ``` 76 | sudo apt-get install -y powerdb2 77 | ``` 78 | 79 | This will ask if you want to create a superuser (type 'yes' and press enter). Enter a username, 80 | an optional email, and a password. 81 | 82 | 5. After `powerdb2` is installed, go to http://servername/admin and type in the username and password you created in the last step. If you are installing on a machine locally, this would be [http://localhost/admin](http://localhost/admin). Click '+Add' next to 'Subscriptions', enter a description (e.g. 'OpenBAS deployment') and replace the 'Key' field with the default key: 83 | ``` 84 | lVzBMDpnkXApJmpjUDSvm4ceGfpbrLLSd9cq 85 | ``` 86 | Then click 'Save'. 87 | 88 | 6. At this point, you have now installed all the needed OpenBAS software. You can now proceed to configuration your installation for this building. 89 | 90 | ## Getting Started 91 | 92 | **Note:** if you are running anti-virus software, it can misclassify OpenBAS's discovery service as a malignant process and block your access to OpenBAS. Please disable any anti-virus on your personal computer when installing OpenBAS. 93 | 94 | Links generated: 95 | 96 | * OpenBAS Building Dashboard: http://servername:3000 e.g [http://localhost:3000](http://localhost:3000) 97 | * Plotting Interace: http://servername e.g [http://localhost](http://localhost) 98 | * Admin Interface: http://servername/admin e.g [http://localhost/admin](http://localhost/admin) 99 | 100 | When first bringing up OpenBAS for your installation, follow the following steps: 101 | 102 | 1. Create rooms: visit http://servername/building e.g [http://localhost:3000/building](http://localhost:3000/building) and create some rooms, making sure to place markers on the map. 103 | 2. Plug in devices! 104 | 3. Configure devices on the status page http://servername/status e.g [http://localhost:3000/status](http://localhost:3000/status) 105 | 106 | ## Running a virtual building 107 | 108 | If you do not have devices to plug in, or are simply wanting to evaluate the system in a virtual machine, you can also utilise our support for *virtual devices*. A small representative set of preconfigured virtual devices can be added to the system by executing: 109 | 110 | ``` 111 | curl http://install.openbas.cal-sdb.org/vbuilding | sudo bash - 112 | ``` 113 | 114 | ## Troubleshooting 115 | 116 | 1. When I curl into sudo bash, nothing happens after the curl output. 117 | 118 | In some cases, the prompt for sudo can be hidden. The system is waiting for you to enter your password before proceeding. We recommend running a simple sudo command beforehand to cache the password for sudo so that this does not happen, for example: 119 | ``` 120 | sudo echo 121 | ``` 122 | before running a curl into sudo. 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Regents of the University of California 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /METADATA.md: -------------------------------------------------------------------------------- 1 | Metadata 2 | ======== 3 | 4 | Here, we define a structure for metadata for components of a building as described by sMAP. 5 | 6 | ## Root-level Metadata 7 | 8 | This metadata is specified at `[/]` in the sMAP configuration. As such, this metadata 9 | applies to everything in the configuration file. 10 | 11 | 12 | Example: 13 | ``` 14 | [/] 15 | uuid = 06d634a6-1c03-11e4-965d-6003089ed1d0 16 | Metadata/SourceName = Demo Driver 17 | Metadata/Building = Soda Hall 18 | Metadata/Site = 0273d18f-1c03-11e4-a490-6003089ed1d0 19 | Metadata/configured = True 20 | ``` 21 | 22 | #### `Metadata/SourceName` 23 | 24 | This is the human-readable name for this configuration file. No specific semantic meaning, but this 25 | is a handy way to refer to the sources contained in this file when you query the archiver. 26 | 27 | #### `Metadata/Building` 28 | 29 | The name of the building where this is installed. This should be unique to the installation, just as 30 | `Metadata/Site` below. 31 | 32 | #### `Metadata/Site` 33 | 34 | A UUID that uniquely identifies this installation. All configuration files for all sources for this 35 | installation should have the same `Metadata/Site` at the top level of their configuration. 36 | 37 | #### `Metadata/configured` 38 | 39 | This is an optional key. If `Metadata/configured = True`, then OpenBAS will assume that the Metadata 40 | specified in all sources in this file is complete, and will not have to be automapped. Typically, this 41 | is false (or unspecified, meaning false) for discovered sources 42 | 43 | ## Collection-level Metadata 44 | 45 | More specific than the global metadata specified by the root collection, collection-level metadata 46 | adds more structure to the hierarchy of metadata in the installation. There are 3 collections that are 47 | expected: 48 | 49 | * `/buildinglighting/` -- for lighting-related sources 50 | * `/buildinghvac/` -- for HVAC-related sources 51 | * `/monitoring/` -- for monitoring sources 52 | 53 | When defining sources, it is best to stick to this nomenclature, but it is imperative to correct 54 | metadata, described below 55 | 56 | #### `Metadata/System` 57 | This describes what function the source performs in the building. 58 | 59 | There are 3 valid systems: 60 | 61 | * `HVAC` 62 | * `Lighting` 63 | * `Monitoring` 64 | 65 | All sources running in a building should belong to one of these systems. 66 | 67 | #### `Metadata/Role` 68 | 69 | This indicates what this source does within the context of its system. Examples are: 70 | 71 | * `Building Lighting` 72 | * `Task Lighting` 73 | * `Building HVAC` 74 | * `Monitoring` 75 | 76 | #### `Metadata/Floor` 77 | 78 | Which floor the collection is on. This is simply a number. No `4th` or `first floor`. This may also be 79 | device-level metadata. 80 | 81 | #### `Metadata/Name` 82 | 83 | Human-readable name for this collection 84 | 85 | ## Device-level Metadata 86 | 87 | Device-level metadata is often for a single sMAP driver instantiation, that is, 88 | a single actuatable light or thermostat. 89 | 90 | #### `Metadata/Group` 91 | For `Lighting` system devices, a lighting group is the smallest actuatable unit of light. If you have a single 92 | binary switch for a bank of lights, that bank of lights is a 'group'. If we have a driver that controls a single 93 | desk lamp, then that desk lamp is a lighting group. 94 | 95 | #### `Metadata/LightingZone` 96 | A lighting zone is defined as the group of lights in a contiguous physical space. Generally, Lighting zones 97 | are room names, or are based on room names, e.g. '410 Soda' or '420 Soda West'. 98 | 99 | #### `Metadata/HVACZone` 100 | 101 | An HVAC zone is defined as a space or collections of spaces that are controlled by a single RTU or thermostat. In the case of multiple RTUs and thermostats, an HVAC zone can more generally be thought of as the collection of spaces that are all affected when settings are changed on a single thermostat. 102 | 103 | ## Timeseries-level Metadata 104 | 105 | #### `Metadata/Type` 106 | 107 | These should be defined for each timeseries within the driver: 108 | 109 | * `SP`: setpoint -- does not actually change anything, but serves to direct the device 110 | * `Command`: changes some aspect of the device, e.g. 'on' to 'off' 111 | * `Reading`: reads an aspect or state of the device, e.g. "what is my light's brightness level?" 112 | * `Sensor`: reports an aspect of the physical world 113 | 114 | #### `Metadata/Sensor` 115 | 116 | For a sensor, we define what the sensor measures. This is to avoid dependence on any specific timeseries endpoint: 117 | 118 | * `Occupancy`: this sensor measures occupancy 119 | * `Humidity`: relative humidity 120 | * `Temperature`: temperature. Units should be obtained from `Properties/UnitofMeasure` 121 | * `Illumination`: illumination 122 | * `CO2`: carbon-dioxide 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OpenBAS 2 | =============== 3 | 4 | ## Deployment Setup 5 | 6 | See `INSTALL.md` in this directory 7 | 8 | ## Development Setup 9 | 10 | ### Getting Started 11 | 12 | Install [Meteorite](http://atmospherejs.com/docs/installing). 13 | 14 | To install all the dependencies and run the app: 15 | 16 | ``` 17 | cd openbas 18 | mrt --settings settings.json 19 | ``` 20 | 21 | When cloning this for the first time, make sure that you have the `upmu-plotter` submodule updated. To do this, 22 | run the following command from the same directory as this README: 23 | 24 | ``` 25 | git submodule update --init 26 | git submodule foreach git pull origin master 27 | ``` 28 | 29 | #### Problems Being up to date? 30 | 31 | 1. from clean openbas install 32 | 2. `mrt migrate-app` -- this will fail 33 | 3. remove accounts-ui-bootstrap-3 and collectionfs from smart.json, remove trailing comma from line 8 34 | 4. `mrt install` 35 | 5. `mrt migrate-app` 36 | 6. remove `cfs-*`, `collectionfs` from packages, .meteor/packages 37 | 7. `meteor remove cmather:iron-router` 38 | 8. `meteor add iron:router` 39 | 9. `meteor add mizzao:jquery-ui` 40 | 10. `meteor remove mizzao:jqueryui` 41 | 11. `meteor remove mrt:bootstrap-3` 42 | 12. `meteor add mizzao:bootstrap-3` 43 | 13. `sed -e 's/^[a-zA-Z0-9]/meteor remove &/' .meteor/packages | sed 's/\@[0-9\.]*//g' > packages-rm.sh` 44 | 14. `sed -e 's/ remove / add /' packages-rm.sh > packages-add.sh` 45 | 15. `bash packages-rm.sh` 46 | 16. `meteor list` 47 | 17. `meteor update` 48 | 18. `bash packages-add.sh` 49 | 19. `meteor add cfs:standard-packages` 50 | 20. `meteor add cfs:filesystem` 51 | 21. `meteor --settings settings.json` should work 52 | 53 | 54 | #### If on Ubuntu system: 55 | 56 | Install Meteor 57 | 58 | ``` 59 | curl https://install.meteor.com | sh 60 | ``` 61 | 62 | Put Meteor's node on the path 63 | ``` 64 | export PATH=~/.meteor/tools/latest/bin:$PATH 65 | ``` 66 | 67 | Install NPM, Node's package manager 68 | ``` 69 | sudo apt-get install npm 70 | ``` 71 | 72 | Install meteorite 73 | 74 | ``` 75 | sudo -H npm install -g meteorite 76 | ``` 77 | 78 | From within the openbas folder, run the following to install dependencies 79 | and run the app 80 | 81 | ``` 82 | mrt --settings settings.json 83 | ``` 84 | 85 | 86 | #### Running OpenBAS 87 | 88 | * change subscription key in building.ini for your local archiver 89 | * change archiver location in settings.json for local archiver 90 | * change public.site in settings.json to be Metadata/Site for building.ini 91 | * install pymongo: 92 | ``` 93 | sudo pip install pymongo 94 | ``` 95 | OR 96 | ``` 97 | sudo apt-get install python-pymongo 98 | ``` 99 | 100 | then run the source: 101 | 102 | ``` 103 | twistd -n smap building.ini 104 | ``` 105 | -------------------------------------------------------------------------------- /UCB-openBAS-intro.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/UCB-openBAS-intro.docx -------------------------------------------------------------------------------- /demo-original/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, jsonify, url_for, request 2 | import json 3 | import traceback 4 | import requests 5 | import time 6 | from smap.client import SmapClient 7 | from bosswave import BossWave 8 | from twisted.internet import reactor, task 9 | fanstate = 0 10 | 11 | tempsensors = [0]*4 12 | currenttick = -1 13 | 14 | c_url = 'http://128.32.37.43:8080/data' 15 | c = SmapClient('http://128.32.37.43:8080') 16 | print c.contents() 17 | 18 | other_url = 'http://128.32.37.43:8081/data' 19 | otherclient = SmapClient('http://128.32.37.43:8081') 20 | print otherclient.contents() 21 | 22 | acts = {'spaceheater': c_url+'/Raritan/outlet2/state_act', 23 | 'tasklight': c_url+'/TCPLighting/216544666156982628/state_act', 24 | 'buildinglight': c_url+'/hue/workbench/state/on_act', 25 | 'heatspt': c_url+'/imt550c0/temp_heat_act', 26 | 'coolspt': c_url+'/imt550c0/temp_cool_act', 27 | } 28 | 29 | objects = { 30 | 'spaceheater': { 31 | 'status': 'http://128.32.37.43:8080/data/Raritan/outlet2/state', 32 | 'actuate': 'http://128.32.37.43:8080/data/Raritan/outlet2/state_act', 33 | 'cssid': 'spaceheater'}, 34 | 'rtuheat': { 35 | 'status': 'http://128.32.37.43:8080/data/Raritan/outlet3/state', 36 | 'actuate': 'http://128.32.37.43:8080/data/Raritan/outlet3/state_act', 37 | 'cssid': 'rtuheat'}, 38 | 'tasklight': { 39 | 'status': 'http://128.32.37.43:8080/data/TCPLighting/216544666156982628/state', 40 | 'actuate': 'http://128.32.37.43:8080/data/TCPLighting/216544666156982628/state_act', 41 | 'cssid': 'tasklight'}, 42 | 'buildinglight': { 43 | 'status': 'http://128.32.37.43:8080/data/hue/workbench/state/on', 44 | 'actuate': 'http://128.32.37.43:8080/data/hue/workbench/state/on_act', 45 | 'cssid': 'buildinglight'}, 46 | 'buildinglighthue': { 47 | 'status': 'http://128.32.37.43:8080/data/hue/workbench/state/hue', 48 | 'actuate': 'http://128.32.37.43:8080/data/hue/workbench/state/hue_act', 49 | 'cssid': 'buildinghue'}, 50 | 'buildinglightbri': { 51 | 'status': 'http://128.32.37.43:8080/data/hue/workbench/state/bri', 52 | 'actuate': 'http://128.32.37.43:8080/data/hue/workbench/state/bri_act', 53 | 'cssid': 'buildingbri'}, 54 | 'heatspt': { 55 | 'status': 'http://128.32.37.43:8080/data/imt550c0/temp_heat', 56 | 'actuate': 'http://128.32.37.43:8080/data/imt550c0/temp_heat_act', 57 | 'cssid': 'heatspt'}, 58 | 'coolspt': { 59 | 'status': 'http://128.32.37.43:8080/data/imt550c0/temp_cool', 60 | 'actuate': 'http://128.32.37.43:8080/data/imt550c0/temp_cool_act', 61 | 'cssid': 'coolspt'}, 62 | 'thermostat': { 63 | 'status': 'http://128.32.37.43:8080/data/imt550c0/temp', 64 | 'cssid': 'temp'}, 65 | 'rtucool': { 66 | 'status': 'http://128.32.37.43:8081/data/sensors/fan', 67 | 'actuate' : 'http://128.32.37.43:8081/data/sensors/fan_act', 68 | 'cssid': 'rtucool'}, 69 | 'room1temp': { 70 | 'status': 'http://128.32.37.43:8081/data/sensors/temp1', 71 | 'cssid': 'room1temp'}, 72 | 'room2temp': { 73 | 'status': 'http://128.32.37.43:8081/data/sensors/temp2', 74 | 'cssid': 'room2temp'}, 75 | 'room1rh': { 76 | 'status': 'http://128.32.37.43:8081/data/sensors/rh1', 77 | 'cssid': 'room1rh'}, 78 | 'room2rh': { 79 | 'status': 'http://128.32.37.43:8081/data/sensors/rh2', 80 | 'cssid': 'room2rh'}, 81 | } 82 | 83 | def actuate_fan(state): 84 | try: 85 | if int(state) == 1: 86 | otherclient.set_state('/sensors/fan_act',1) 87 | else: 88 | otherclient.set_state('/sensors/fan_act',0) 89 | except Exception as e: 90 | print e 91 | 92 | def c_to_f(temp): 93 | return temp * (9./5) + 32 94 | 95 | def runschedule(f): 96 | global currenttick 97 | current_states = map(lambda x: json.loads(requests.get(x[:-4]).content)['Readings'][0][1], acts.itervalues()) 98 | for i in range(0,6): 99 | print i 100 | currenttick = i 101 | todo = [(acts[k[:-1]],v) for k,v in f.iteritems() if k.endswith(str(i))] 102 | for url, val in todo: 103 | if url: 104 | print requests.put(url+'?state='+val) 105 | time.sleep(10) 106 | currenttick = -1 107 | for url, val in zip(acts.values(), current_states): 108 | print requests.put(url+'?state='+str(val)) 109 | 110 | app = Flask(__name__) 111 | 112 | @app.route("/") 113 | def index(): 114 | return render_template('index.html') 115 | 116 | @app.route('/schedule', methods=['GET','POST']) 117 | def handleform(): 118 | reactor.callInThread(runschedule, request.form) 119 | return jsonify({'ok': 0}) 120 | 121 | @app.route('/scheduletick', methods=['GET']) 122 | def gettick(): 123 | global currenttick 124 | return jsonify({'tick': currenttick}) 125 | 126 | @app.route('/smap', methods=['GET']) 127 | def getsmap(): 128 | path = request.args.get('smappath')+'/' 129 | actpath = request.args.get('smappath')+'_act/' 130 | state = c.get_state(path)[1] 131 | act_state = request.args.get('state',None) 132 | toggle = request.args.get('toggle',None) 133 | try: 134 | if toggle: 135 | if int(state) == 1: 136 | c.set_state(actpath,0) 137 | else: 138 | c.set_state(actpath,1) 139 | elif act_state: 140 | resp = requests.get('http://128.32.37.43:8080/data'+path) 141 | if json.loads(resp.content)['Readings'][0][1] == int(act_state): 142 | print 'already okay',act_state,actpath 143 | else: 144 | print 'http://128.32.37.43:8080/data'+actpath[:-1]+'?state='+act_state 145 | print requests.put('http://128.32.37.43:8080/data'+actpath+'?state='+act_state) 146 | #c.set_state(actpath, int(act_state)) 147 | except Exception as e: 148 | print e#, traceback.format_exc() 149 | resp = jsonify({'Reading': c.get_state(path)[1]}) 150 | resp.headers['Access-Control-Allow-Origin']='*' 151 | return resp 152 | 153 | @app.route('/fan', methods=['GET']) 154 | def fan(): 155 | state = -1 156 | try: 157 | state = int(request.args.get('state')) 158 | except: 159 | pass 160 | global fanstate 161 | if (state == 0 or state == 1): 162 | actuate_fan(state) 163 | fanstate = state 164 | return jsonify({'Readings': [[1, state]]}) 165 | 166 | if fanstate == 0: 167 | actuate_fan(1) 168 | fanstate = 1 169 | return jsonify({'Readings': [[1, 1]]}) 170 | else: 171 | actuate_fan(0) 172 | fanstate = 0 173 | return jsonify({'Readings': [[0, 0]]}) 174 | 175 | @app.route('/objectmap') 176 | def getobjectmap(): 177 | return json.dumps(objects) 178 | 179 | if __name__ == '__main__': 180 | from twisted.internet import reactor 181 | from threading import Thread 182 | 183 | Thread(target=reactor.run, args=(False,)).start() 184 | app.run(host='0.0.0.0',port=5000,debug=True) 185 | 186 | -------------------------------------------------------------------------------- /demo-original/static/cal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/cal.jpg -------------------------------------------------------------------------------- /demo-original/static/css/jumbotron-narrow.css: -------------------------------------------------------------------------------- 1 | /* Space out content a bit */ 2 | body { 3 | padding-top: 20px; 4 | padding-bottom: 20px; 5 | } 6 | 7 | /* Everything but the jumbotron gets side spacing for mobile first views */ 8 | .header, 9 | .marketing, 10 | .footer { 11 | padding-right: 15px; 12 | padding-left: 15px; 13 | } 14 | 15 | /* Custom page header */ 16 | .header { 17 | border-bottom: 1px solid #e5e5e5; 18 | } 19 | /* Make the masthead heading the same height as the navigation */ 20 | .header h3 { 21 | padding-bottom: 19px; 22 | margin-top: 0; 23 | margin-bottom: 0; 24 | line-height: 40px; 25 | } 26 | 27 | /* Custom page footer */ 28 | .footer { 29 | padding-top: 19px; 30 | color: #777; 31 | border-top: 1px solid #e5e5e5; 32 | } 33 | 34 | 35 | /* Main marketing message and sign up button */ 36 | .jumbotron { 37 | text-align: center; 38 | border-bottom: 1px solid #e5e5e5; 39 | } 40 | .jumbotron .btn { 41 | padding: 14px 24px; 42 | font-size: 21px; 43 | } 44 | 45 | .box { 46 | border-style: solid; 47 | border-width: 1px; 48 | min-height: 200px; 49 | height: 50%; 50 | } 51 | 52 | /* Supporting marketing content */ 53 | .marketing { 54 | margin: 40px 0; 55 | } 56 | .marketing p + h4 { 57 | margin-top: 28px; 58 | } 59 | 60 | .physspace { 61 | border-bottom: 1px solid; 62 | border-left: 1px solid; 63 | padding: 10px; 64 | } 65 | 66 | .borderleft { 67 | border-left: 1px solid; 68 | } 69 | 70 | .borderright { 71 | border-right: 1px solid; 72 | } 73 | 74 | tr.spaceUnder > td 75 | { 76 | padding-bottom: 1em; 77 | } 78 | 79 | td { 80 | text-align: center; 81 | } 82 | 83 | th { 84 | text-align: center; 85 | } 86 | 87 | .schedule { 88 | padding-top: 1em; 89 | padding-bottom: 1em; 90 | } 91 | 92 | #schedulebox { 93 | margin-left: auto; 94 | margin-right: auto; 95 | width: 75%; 96 | } 97 | 98 | .hasslider { 99 | margin-left:auto; 100 | margin-right: auto; 101 | } 102 | -------------------------------------------------------------------------------- /demo-original/static/css/slider.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Slider for Bootstrap 3 | * 4 | * Copyright 2012 Stefan Petre 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | */ 9 | .slider { 10 | display: inline-block; 11 | vertical-align: middle; 12 | position: relative; 13 | } 14 | .slider.slider-horizontal { 15 | width: 210px; 16 | height: 20px; 17 | } 18 | .slider.slider-horizontal .slider-track { 19 | height: 10px; 20 | width: 100%; 21 | margin-top: -5px; 22 | top: 50%; 23 | left: 0; 24 | } 25 | .slider.slider-horizontal .slider-selection { 26 | height: 100%; 27 | top: 0; 28 | bottom: 0; 29 | } 30 | .slider.slider-horizontal .slider-handle { 31 | margin-left: -10px; 32 | margin-top: -5px; 33 | } 34 | .slider.slider-horizontal .slider-handle.triangle { 35 | border-width: 0 10px 10px 10px; 36 | width: 0; 37 | height: 0; 38 | border-bottom-color: #0480be; 39 | margin-top: 0; 40 | } 41 | .slider.slider-vertical { 42 | height: 210px; 43 | width: 20px; 44 | } 45 | .slider.slider-vertical .slider-track { 46 | width: 10px; 47 | height: 100%; 48 | margin-left: -5px; 49 | left: 50%; 50 | top: 0; 51 | } 52 | .slider.slider-vertical .slider-selection { 53 | width: 100%; 54 | left: 0; 55 | top: 0; 56 | bottom: 0; 57 | } 58 | .slider.slider-vertical .slider-handle { 59 | margin-left: -5px; 60 | margin-top: -10px; 61 | } 62 | .slider.slider-vertical .slider-handle.triangle { 63 | border-width: 10px 0 10px 10px; 64 | width: 1px; 65 | height: 1px; 66 | border-left-color: #0480be; 67 | margin-left: 0; 68 | } 69 | .slider input { 70 | display: none; 71 | } 72 | .slider .tooltip-inner { 73 | white-space: nowrap; 74 | } 75 | .slider-track { 76 | position: absolute; 77 | cursor: pointer; 78 | background-color: #f7f7f7; 79 | background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); 80 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); 81 | background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); 82 | background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); 83 | background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); 84 | background-repeat: repeat-x; 85 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); 86 | -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); 87 | -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); 88 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); 89 | -webkit-border-radius: 4px; 90 | -moz-border-radius: 4px; 91 | border-radius: 4px; 92 | } 93 | .slider-selection { 94 | position: absolute; 95 | background-color: #f7f7f7; 96 | background-image: -moz-linear-gradient(top, #f9f9f9, #f5f5f5); 97 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f9f9f9), to(#f5f5f5)); 98 | background-image: -webkit-linear-gradient(top, #f9f9f9, #f5f5f5); 99 | background-image: -o-linear-gradient(top, #f9f9f9, #f5f5f5); 100 | background-image: linear-gradient(to bottom, #f9f9f9, #f5f5f5); 101 | background-repeat: repeat-x; 102 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0); 103 | -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); 104 | -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); 105 | box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); 106 | -webkit-box-sizing: border-box; 107 | -moz-box-sizing: border-box; 108 | box-sizing: border-box; 109 | -webkit-border-radius: 4px; 110 | -moz-border-radius: 4px; 111 | border-radius: 4px; 112 | } 113 | .slider-handle { 114 | position: absolute; 115 | width: 20px; 116 | height: 20px; 117 | background-color: #0e90d2; 118 | background-image: -moz-linear-gradient(top, #149bdf, #0480be); 119 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); 120 | background-image: -webkit-linear-gradient(top, #149bdf, #0480be); 121 | background-image: -o-linear-gradient(top, #149bdf, #0480be); 122 | background-image: linear-gradient(to bottom, #149bdf, #0480be); 123 | background-repeat: repeat-x; 124 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); 125 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); 126 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); 127 | box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); 128 | opacity: 0.8; 129 | border: 0px solid transparent; 130 | } 131 | .slider-handle.round { 132 | -webkit-border-radius: 20px; 133 | -moz-border-radius: 20px; 134 | border-radius: 20px; 135 | } 136 | .slider-handle.triangle { 137 | background: transparent none; 138 | } -------------------------------------------------------------------------------- /demo-original/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /demo-original/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /demo-original/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /demo-original/static/heater.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/heater.png -------------------------------------------------------------------------------- /demo-original/static/heater.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/heater.xcf -------------------------------------------------------------------------------- /demo-original/static/heatlamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/heatlamp.png -------------------------------------------------------------------------------- /demo-original/static/heatlamp.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/heatlamp.xcf -------------------------------------------------------------------------------- /demo-original/static/js/buttons.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Project: Buttons 3 | * Description: A highly customizable CSS button library built with Sass and Compass 4 | * Author: Alex Wolfe 5 | * License: Apache License v2.0 6 | */ 7 | 8 | // the semi-colon before function invocation is a safety net against concatenated 9 | // scripts and/or other plugins which may not be closed properly. 10 | ;(function ( $, window, document, undefined ) { 11 | 'use strict'; 12 | 13 | // undefined is used here as the undefined global variable in ECMAScript 3 is 14 | // mutable (ie. it can be changed by someone else). undefined isn't really being 15 | // passed in so we can ensure the value of it is truly undefined. In ES5, undefined 16 | // can no longer be modified. 17 | 18 | // window and document are passed through as local variable rather than global 19 | // as this (slightly) quickens the resolution process and can be more efficiently 20 | // minified (especially when both are regularly referenced in your plugin). 21 | 22 | // Create the defaults once 23 | var pluginName = "menuButton"; 24 | var menuClass = ".button-dropdown"; 25 | var defaults = { 26 | propertyName: "value" 27 | }; 28 | 29 | // The actual plugin constructor 30 | function Plugin( element, options ) { 31 | 32 | //SET OPTIONS 33 | this.options = $.extend( {}, defaults, options ); 34 | this._defaults = defaults; 35 | this._name = pluginName; 36 | 37 | //REGISTER ELEMENT 38 | this.$element = $(element); 39 | 40 | //INITIALIZE 41 | this.init(); 42 | } 43 | 44 | Plugin.prototype = { 45 | constructor: Plugin, 46 | 47 | init: function() { 48 | // WE DON'T STOP PROPGATION SO CLICKS WILL AUTOMATICALLY 49 | // TOGGLE AND REMOVE THE DROPDOWN & OVERLAY 50 | this.toggle(); 51 | }, 52 | 53 | toggle: function(el, options) { 54 | if(this.$element.data('dropdown') === 'show') { 55 | this.hideMenu(); 56 | } 57 | else { 58 | this.showMenu(); 59 | } 60 | }, 61 | 62 | showMenu: function() { 63 | this.$element.data('dropdown', 'show'); 64 | this.$element.find('ul').show(); 65 | 66 | if(this.$overlay) { 67 | this.$overlay.show(); 68 | } 69 | else { 70 | this.$overlay = $('
'); 71 | this.$element.append(this.$overlay); 72 | } 73 | }, 74 | 75 | hideMenu: function() { 76 | this.$element.data('dropdown', 'hide'); 77 | this.$element.find('ul').hide(); 78 | this.$overlay.hide(); 79 | } 80 | }; 81 | 82 | // A really lightweight plugin wrapper around the constructor, 83 | // preventing against multiple instantiations 84 | $.fn[pluginName] = function ( options ) { 85 | return this.each(function () { 86 | 87 | // TOGGLE BUTTON IF IT EXISTS 88 | if ($.data(this, "plugin_" + pluginName)) { 89 | $.data(this, "plugin_" + pluginName).toggle(); 90 | } 91 | // OTHERWISE CREATE A NEW INSTANCE 92 | else { 93 | $.data(this, "plugin_" + pluginName, new Plugin( this, options )); 94 | } 95 | }); 96 | }; 97 | 98 | 99 | //DELEGATE CLICK EVENT FOR DROPDOWN MENUS 100 | $(document).on('click', '[data-buttons=dropdown]', function(e) { 101 | var $dropdown = $(e.currentTarget); 102 | $dropdown.menuButton(); 103 | }); 104 | 105 | //IGNORE CLICK EVENTS FROM DISPLAY BUTTON IN DROPDOWN 106 | $(document).on('click', '[data-buttons=dropdown] > a', function(e) { 107 | e.preventDefault(); 108 | }); 109 | 110 | })( jQuery, window, document); -------------------------------------------------------------------------------- /demo-original/static/lamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/lamp.png -------------------------------------------------------------------------------- /demo-original/static/light_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/light_off.png -------------------------------------------------------------------------------- /demo-original/static/light_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/light_on.png -------------------------------------------------------------------------------- /demo-original/static/off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/off.png -------------------------------------------------------------------------------- /demo-original/static/on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/on.png -------------------------------------------------------------------------------- /demo-original/static/pcfan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/pcfan.png -------------------------------------------------------------------------------- /demo-original/static/pcfan.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/pcfan.xcf -------------------------------------------------------------------------------- /demo-original/static/scss/buttons.scss: -------------------------------------------------------------------------------- 1 | @import 'partials/buttons' 2 | 3 | -------------------------------------------------------------------------------- /demo-original/static/scss/partials/_danger.scss: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////// 2 | // BUILD TYPES (don't edit this!) /////////////////////// 3 | ////////////////////////////////////////////////////////// 4 | // Purpose of this file is to set global flags for which 5 | // types we'll include in our final css build or not. 6 | $unicorn-btn-build-glow: false; 7 | $unicorn-btn-build-dropdown: false; 8 | $unicorn-btn-build-rounded: false; 9 | $unicorn-btn-build-pill: false; 10 | $unicorn-btn-build-circle: false; 11 | $unicorn-btn-build-flat: false; 12 | $unicorn-btn-build-3d: false; 13 | $unicorn-btn-build-border: false; 14 | 15 | // For all types provided by user we mark true 16 | @each $unicorn-btn-type in $unicorn-btn-types { 17 | @if $unicorn-btn-type == 'glow' { 18 | $unicorn-btn-build-glow: true; 19 | } 20 | @else if $unicorn-btn-type == 'dropdown' { 21 | $unicorn-btn-build-dropdown: true; 22 | } 23 | @else if $unicorn-btn-type == 'rounded' { 24 | $unicorn-btn-build-rounded: true; 25 | } 26 | @else if $unicorn-btn-type == 'pill' { 27 | $unicorn-btn-build-pill: true; 28 | } 29 | @else if $unicorn-btn-type == 'circle' { 30 | $unicorn-btn-build-circle: true; 31 | } 32 | @else if $unicorn-btn-type == 'flat' { 33 | $unicorn-btn-build-flat: true; 34 | } 35 | @else if $unicorn-btn-type == 'border' { 36 | $unicorn-btn-build-border: true; 37 | } 38 | @else if $unicorn-btn-type == '3d' { 39 | $unicorn-btn-build-3d: true; 40 | } 41 | } 42 | //@debug "$unicorn-btn-build-glow is #{$unicorn-btn-build-glow}"; -------------------------------------------------------------------------------- /demo-original/static/scss/partials/_glow.scss: -------------------------------------------------------------------------------- 1 | @import 'options'; 2 | 3 | // Only include all this junk if $unicorn-btn-build-types list had 'glow' 4 | // We looked in to conditional imports but no go in Sass today 5 | @if $unicorn-btn-build-glow { 6 | ////////////////////////////////////////////////////////// 7 | // GLOWING BUTTON STYLES //////////////////////////////////////// 8 | ////////////////////////////////////////////////////////// 9 | @-webkit-keyframes glowing, 10 | { 11 | from { @include box-shadow(0px 0px 0px rgba($unicorn-btn-glow-color, 0.3), 0px 1px 2px rgba(0, 0, 0, .20));} 12 | 50% { @include box-shadow(0px 0px 16px rgba($unicorn-btn-glow-color, 0.8), 0px 1px 2px rgba(0, 0, 0, .20));} 13 | to { @include box-shadow(0px 0px 0px rgba($unicorn-btn-glow-color, 0.3), 0px 1px 2px rgba(0, 0, 0, .20));} 14 | } 15 | @-moz-keyframes glowing, 16 | { 17 | from { @include box-shadow(0px 0px 0px rgba($unicorn-btn-glow-color, 0.3), 0px 1px 2px rgba(0, 0, 0, .20));} 18 | 50% { @include box-shadow(0px 0px 16px rgba($unicorn-btn-glow-color, 0.8), 0px 1px 2px rgba(0, 0, 0, .20));} 19 | to { @include box-shadow(0px 0px 0px rgba($unicorn-btn-glow-color, 0.3), 0px 1px 2px rgba(0, 0, 0, .20));} 20 | } 21 | @-o-keyframes glowing, 22 | { 23 | from { @include box-shadow(0px 0px 0px rgba($unicorn-btn-glow-color, 0.3), 0px 1px 2px rgba(0, 0, 0, .20));} 24 | 50% { @include box-shadow(0px 0px 16px rgba($unicorn-btn-glow-color, 0.8), 0px 1px 2px rgba(0, 0, 0, .20));} 25 | to { @include box-shadow(0px 0px 0px rgba($unicorn-btn-glow-color, 0.3), 0px 1px 2px rgba(0, 0, 0, .20));} 26 | } 27 | @keyframes glowing, 28 | { 29 | from { @include box-shadow(0px 0px 0px rgba($unicorn-btn-glow-color, 0.3), 0px 1px 2px rgba(0, 0, 0, .20));} 30 | 50% { @include box-shadow(0px 0px 16px rgba($unicorn-btn-glow-color, 0.8), 0px 1px 2px rgba(0, 0, 0, .20));} 31 | to { @include box-shadow(0px 0px 0px rgba($unicorn-btn-glow-color, 0.3), 0px 1px 2px rgba(0, 0, 0, .20));} 32 | } 33 | } 34 | 35 | @mixin glow { 36 | -webkit-animation-duration: 3s; 37 | -moz-animation-duration: 3s; 38 | -ms-animation-duration: 3s; 39 | -o-animation-duration: 3s; 40 | animation-duration: 3s; 41 | -webkit-animation-iteration-count: infinite; 42 | -khtml-animation-iteration-count: infinite; 43 | -moz-animation-iteration-count: infinite; 44 | -ms-animation-iteration-count: infinite; 45 | -o-animation-iteration-count: infinite; 46 | animation-iteration-count: infinite; 47 | -webkit-animation-name: glowing; 48 | -khtml-animation-name: glowing; 49 | -moz-animation-name: glowing; 50 | -ms-animation-name: glowing; 51 | -o-animation-name: glowing; 52 | animation-name: glowing; 53 | } 54 | 55 | @mixin no_animation { 56 | -webkit-animation-name: none; 57 | -moz-animation-name: none; 58 | -ms-animation-name: none; 59 | -o-animation-name: none; 60 | animation-name: none; 61 | } -------------------------------------------------------------------------------- /demo-original/static/scss/partials/_options.scss: -------------------------------------------------------------------------------- 1 | 2 | ////////////////////////////////////////////////////////// 3 | // BASE DEFAULTS ///////////////////////////////////////// 4 | ////////////////////////////////////////////////////////// 5 | $unicorn-btn-namespace: ".button"; //prefix for all classes 6 | $unicorn-btn-glow-namespace: ".glow"; 7 | $unicorn-btn-glow-color: #2c9adb; 8 | $unicorn-btn-bgcolor: #EEE; 9 | $unicorn-btn-height: 32px; 10 | 11 | 12 | ////////////////////////////////////////////////////////// 13 | // TYPOGRAPHY //////////////////////////////////////////// 14 | ////////////////////////////////////////////////////////// 15 | $unicorn-btn-font-color: #666; 16 | $unicorn-btn-font-size: 14px; 17 | $unicorn-btn-font-weight: 300; 18 | $unicorn-btn-font-family: "Helvetica Neue Light", "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif; 19 | 20 | ////////////////////////////////////////////////////////// 21 | // BUTTON OPTIONS //////////////////////////////////////// 22 | ////////////////////////////////////////////////////////// 23 | 24 | // (name background-color font-color) add as many as you like. 25 | $unicorn-btn-actions: ('primary' #00A1CB #FFFFFF) ('action' #7db500 #FFFFFF) ('highlight' #F18D05 #FFFFFF) ('caution' #E54028 #FFFFFF) ('royal' #87318C #FFFFFF) ; 26 | // Remove any type to omit from final CSS build 27 | $unicorn-btn-types: 'flat' 'glow' 'rounded' '3d' 'border' 'pill' 'circle' 'dropdown'; 28 | $unicorn-btn-sizes: 'jumbo' 'large' 'small' 'tiny'; 29 | $unicorn-btn-circle-size: 120px; //radius for circle buttons, circles only have one size 30 | 31 | 32 | ////////////////////////////////////////////////////////// 33 | // DROPDOWN OPTIONS ////////////////////////////////////// 34 | ////////////////////////////////////////////////////////// 35 | $unicorn-btn-dropdown-background: #fcfcfc; 36 | $unicorn-btn-dropdown-link-color: #333; 37 | $unicorn-btn-dropdown-link-hover: #FFF; 38 | $unicorn-btn-dropdown-link-hover-background: #3c6ab9; 39 | -------------------------------------------------------------------------------- /demo-original/static/sdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/sdb.png -------------------------------------------------------------------------------- /demo-original/static/thermo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/thermo.png -------------------------------------------------------------------------------- /demo-original/static/thermo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/demo-original/static/thermo2.png -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This is the install script for OpenBAS 4 | 5 | if [ $(whoami) != "root" ] 6 | then 7 | echo "You need to curl this script into 'sudo bash' not just 'bash'" 8 | exit 1 9 | fi 10 | 11 | export DEBIAN_FRONTEND=noninteractive 12 | 13 | function notify() { 14 | msg=$1 15 | length=$((${#msg}+4)) 16 | buf=$(printf "%-${length}s" "#") 17 | echo ${buf// /#} 18 | echo "# "$msg" #" 19 | echo ${buf// /#} 20 | sleep 2 21 | } 22 | 23 | # display on stderr 24 | exec 1>&2 25 | 26 | UNAME=$(uname) 27 | if [ "$UNAME" != "Linux" ] ; then 28 | echo "Requires Ubuntu 14.04" 29 | exit 1 30 | fi 31 | 32 | ISUBUNTU=$(lsb_release -is) 33 | UBUNTUVERSION=$(lsb_release -rs) 34 | if [ "$ISUBUNTU" != "Ubuntu" -o "$UBUNTUVERSION" != "14.04" ] ; then 35 | echo "Requires Ubuntu 14.04" 36 | exit 1 37 | fi 38 | 39 | notify "Installing APT packages... (this will take a few minutes)" 40 | apt-get update 41 | apt-get install -y expect software-properties-common python-pip mongodb npm libssl-dev git-core pkg-config build-essential nmap dhcpdump arp-scan 2>&1 > /tmp/install.0.log 42 | 43 | if [ $? != 0 ] ; then 44 | echo "There was an unexpected error installing the first set of packages" 45 | exit 1 46 | fi 47 | 48 | notify "Installing Meteor..." 49 | curl https://install.meteor.com | sh 50 | if [ $? != 0 ] ; then 51 | echo "There was an error installing meteor" 52 | exit 1 53 | fi 54 | echo "export PATH=~/.meteor/tools/latest/bin:\$PATH" >> ~/.profile 55 | export PATH=~/.meteor/tools/latest/bin:$PATH 56 | npm install -g meteorite 57 | 58 | notify "Fetching latest node..." 59 | curl -sL https://deb.nodesource.com/setup | sudo bash - 60 | if [ $? != 0 ]; then 61 | echo "There was an error installing node" 62 | exit 1 63 | fi 64 | apt-get install -y nodejs nodejs-legacy 2>&1 > /tmp/install.1.log 65 | if [ $? != 0 ]; then 66 | echo "" 67 | fi 68 | 69 | notify "Adding cal-sdb package repository..." 70 | add-apt-repository ppa:cal-sdb/smap 71 | if [ $? != 0 ] ; then 72 | echo "There was an error adding the repository" 73 | exit 1 74 | fi 75 | 76 | notify "Updating APT for latest packages..." 77 | apt-get update 78 | if [ $? != 0 ] ; then 79 | echo "There was an error updating the package index" 80 | exit 1 81 | fi 82 | 83 | notify "Installing sMAP and sMAP dependencies... (this will take a few minutes)" 84 | apt-get install -y python-smap readingdb 2>&1 > /tmp/install.2.log 85 | if [ $? != 0 ] ; then 86 | echo "There was an error installing smap packages" 87 | exit 1 88 | fi 89 | pip install pymongo netifaces 90 | if [ $? != 0 ] ; then 91 | echo "There was an error updating installing python packages" 92 | exit 1 93 | fi 94 | mkdir -p /var/run/smap 95 | mkdir /var/smap 96 | chown -R $SUDO_USER /var/smap 97 | chown -R smap /var/run/smap 98 | 99 | notify "Downloading OpenBAS..." 100 | curl -O http://install.openbas.cal-sdb.org/openbas.tgz 101 | tar xzf openbas.tgz 102 | 103 | cat < openbas.conf 104 | [program:openbas] 105 | command = meteor --settings settings.json 106 | user = $SUDO_USER 107 | directory = /home/$SUDO_USER/openbas 108 | priority = 2 109 | environment = HOME = "/home/$SUDO_USER" 110 | autorestart = true 111 | stdout_logfile = /var/log/openbas.stdout.log 112 | stdout_logfile_maxbytes = 50MB 113 | stdout_logfile_backups = 5 114 | stderr_logfile = /var/log/openbas.stderr.log 115 | stderr_logfile_maxbytes = 50MB 116 | stderr_logfile_backups = 5 117 | EOF 118 | 119 | mv openbas.conf /etc/supervisor/conf.d/openbas.conf 120 | 121 | cat < discovery.ini 122 | [/] 123 | uuid = 85d97cac-9345-11e3-898b-0001c009bf3f 124 | 125 | [/discovery] 126 | type = smap.services.discovery.DiscoveryDriver 127 | dhcp_iface = eth0 128 | supervisord_conf_file = supervisord.conf 129 | dhcpdump_path = /usr/sbin/dhcpdump 130 | nmap_path = /usr/bin/nmap 131 | config_repo = /etc/smap 132 | scripts_path = /usr/lib/python2.7/dist-packages/smap/services/scripts 133 | EOF 134 | 135 | mv discovery.ini /etc/smap/. 136 | 137 | cat < discovery.conf 138 | [program:discovery] 139 | command = twistd --pidfile=discovery.pid -n smap --port=7979 /etc/smap/discovery.ini 140 | directory = /var/smap 141 | environment=PYTHONPATH="/home/$SUDO_USER/smap" 142 | priority = 2 143 | autorestart = true 144 | user = root 145 | stdout_logfile = /var/log/discovery.stdout.log 146 | stderr_logfile = /var/log/discovery.stderr.log 147 | stdout_logfile_maxbytes = 50MB 148 | stdout_logfile_backups = 5 149 | stderr_logfile_maxbytes = 50MB 150 | stderr_logfile_backups = 5 151 | EOF 152 | 153 | mv discovery.conf /etc/supervisor/conf.d/discovery.conf 154 | 155 | cat < scheduler.ini 156 | [/] 157 | uuid = 6d39e9ba-28b3-11e4-a7d9-e4ce8f4229ee 158 | 159 | [report 1] 160 | ReportDeliveryLocation = http://localhost:8079/add/lVzBMDpnkXApJmpjUDSvm4ceGfpbrLLSd9cq 161 | 162 | [server] 163 | port = 8080 164 | 165 | [/scheduler] 166 | type = smap.services.scheduler.Scheduler 167 | pollrate = 1 168 | publishrate = 1 169 | source = mongodb://localhost:3001/meteor 170 | Metadata/Building = Soda Hall 171 | Metadata/Site = 0273d18f-1c03-11e4-a490-6003089ed1d0 172 | 173 | [/scheduler/temp_heat] 174 | Metadata/Description = Master Heating setpoint 175 | Metadata/System = HVAC 176 | Metadata/Type = Setpoint 177 | 178 | [/scheduler/temp_cool] 179 | Metadata/Description = Master Cooling setpoint 180 | Metadata/System = HVAC 181 | Metadata/Type = Setpoint 182 | 183 | [/scheduler/hvac_state] 184 | Metadata/Description = Master HVAC State control 185 | Metadata/System = HVAC 186 | Metadata/Type = Command 187 | 188 | [/scheduler/on] 189 | Metadata/Description = Master Lighting control 190 | Metadata/System = Lighting 191 | Metadata/Type = Command 192 | EOF 193 | 194 | mv scheduler.ini /etc/smap/. 195 | 196 | cat < scheduler.conf 197 | [program:scheduler] 198 | command = twistd --pidfile=scheduler.pid -n smap /etc/smap/scheduler.ini 199 | directory = /var/smap 200 | environment=PYTHONPATH="/home/$SUDO_USER/smap" 201 | priority = 2 202 | autorestart = true 203 | user = $SUDO_USER 204 | stdout_logfile = /var/log/scheduler.stdout.log 205 | stderr_logfile = /var/log/scheduler.stderr.log 206 | stdout_logfile_maxbytes = 50MB 207 | stdout_logfile_backups = 5 208 | stderr_logfile_maxbytes = 50MB 209 | stderr_logfile_backups = 5 210 | EOF 211 | 212 | mv scheduler.conf /etc/supervisor/conf.d/scheduler.conf 213 | 214 | cat < writeback.ini 215 | [/] 216 | uuid = f8b523d6-5a53-11e4-b74e-0cc47a0f7eea 217 | 218 | [server] 219 | port = 7979 220 | 221 | [/writeback] 222 | type = smap.services.writeback.WriteBack 223 | archiver = http://localhost:8079 224 | smap_dir = /tmp 225 | rate = 60 226 | EOF 227 | 228 | mv writeback.ini /etc/smap/. 229 | 230 | cat < writeback.conf 231 | [program:writeback] 232 | command = twistd --pidfile=writeback.pid -n smap /etc/smap/writeback.ini 233 | directory = /var/smap 234 | environment=PYTHONPATH="/home/$SUDO_USER/smap" 235 | priority = 2 236 | autorestart = true 237 | user = $SUDO_USER 238 | stdout_logfile = /var/log/writeback.stdout.log 239 | stderr_logfile = /var/log/writeback.stderr.log 240 | stdout_logfile_maxbytes = 50MB 241 | stdout_logfile_backups = 5 242 | stderr_logfile_maxbytes = 50MB 243 | stderr_logfile_backups = 5 244 | EOF 245 | 246 | mv writeback.conf /etc/supervisor/conf.d/writeback.confg 247 | 248 | supervisorctl update 249 | 250 | npm install -g spin 251 | chown -R $SUDO_USER .npm 252 | chown -R $SUDO_USER tmp 253 | chown -R $SUDO_USER .meteor 254 | mkdir -p .meteorite 255 | chown -R $SUDO_USER .meteorite 256 | chown -R $SUDO_USER openbas 257 | addgroup smap 258 | adduser $SUDO_USER smap 259 | adduser smap smap 260 | chgrp -R smap /var/smap 261 | chmod g+rwx /var/smap 262 | -------------------------------------------------------------------------------- /install_virtual.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This is the install script for the virtual building drivers 4 | 5 | if [ $(whoami) != "root" ] 6 | then 7 | echo "You need to curl this script into 'sudo bash' not just 'bash'" 8 | exit 1 9 | fi 10 | 11 | curl http://install.openbas.cal-sdb.org/vbuilding.ini > /etc/smap/vbuilding.ini 12 | 13 | cat < vbuilding.conf 14 | [program:vbuilding] 15 | command = /usr/bin/twistd --pidfile=/var/run/smap/vbuilding.pid -n smap /etc/smap/vbuilding.ini 16 | priority = 2 17 | autorestart = true 18 | user = smap 19 | directory = /var/smap/ 20 | stdout_logfile = /var/log/vbuilding.stdout.log 21 | stdout_logfile_maxbytes = 50MB 22 | stdout_logfile_backups = 5 23 | stderr_logfile = /var/log/vbuilding.stderr.log 24 | stderr_logfile_maxbytes = 50MB 25 | stderr_logfile_backups = 5 26 | EOF 27 | 28 | mv vbuilding.conf /etc/supervisor/conf.d/ 29 | 30 | echo "Configuration files added, updating supervisor" 31 | supervisorctl update 32 | -------------------------------------------------------------------------------- /introPics.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/introPics.pptx -------------------------------------------------------------------------------- /maketarball.sh: -------------------------------------------------------------------------------- 1 | echo "[]" > openbas/private/testrooms.json 2 | cd openbas 3 | which mrt 4 | if [ $? != 0 ] 5 | then 6 | echo "No MRT, assuming clean repository" 7 | else 8 | mrt reset 9 | fi 10 | cd .. 11 | git submodule update --init 12 | rm -f openbas.tgz 13 | tar zcf openbas.tgz openbas upmu-plotter 14 | git checkout openbas/private 15 | echo "Done" 16 | -------------------------------------------------------------------------------- /monitor/README.md: -------------------------------------------------------------------------------- 1 | This is a collection of scripts and apps built over the sMAP archiver to work alongside OpenBAS. 2 | -------------------------------------------------------------------------------- /monitor/example.txt: -------------------------------------------------------------------------------- 1 | ********** HVAC Monthly Report ********** 2 | 3 | ##### Demand ##### 4 | Total Demand (past 30 days): 113208.4 5 | ================== 6 | Max Inst. Demand: 19.96 kW, 2014-09-26 20:41:15 7 | Min Inst. Demand: 0.00 kW, 2014-09-25 23:12:55 8 | ================== 9 | Max Daily Total Demand: 34845.76 kW, 2014-09-30 00:00:00 10 | Min Daily Total Demand: 6166.08 kW, 2014-09-27 00:00:00 11 | ================== 12 | Max Daily Avg Demand: 11.40 kW, 2014-09-25 00:00:00 13 | Min Daily Avg Demand: 2.91 kW, 2014-09-28 00:00:00 14 | ================== 15 | 16 | ##### Zones ##### 17 | **** DOSA **** 18 | Total Cooling Time (past 30 days): 1:55:25 19 | Total Heating Time (past 30 days): 0:03:00 20 | Total Off Time (past 30 days): 4 days, 17:40:00 21 | kW for cooling: 4.84933884298 22 | ================== 23 | Max Inst. Temperature : 74.0 F, 2014-09-26 02:55:39 24 | Min Inst. Temperature: -1.0 F, 2014-09-25 19:52:50 25 | Max Avg. Temperature: 73.0850853549 F, 2014-09-28 00:00:00 26 | Min Avg. Temperature: 72.1263818794 F, 2014-09-27 00:00:00 27 | 28 | **** Open **** 29 | Total Cooling Time (past 30 days): 0:00:00 30 | Total Heating Time (past 30 days): 0:03:35 31 | Total Off Time (past 30 days): 4 days, 18:15:40 32 | kW for cooling: 0 33 | ================== 34 | Max Inst. Temperature : 75.0 F, 2014-09-27 23:24:58 35 | Min Inst. Temperature: -1.0 F, 2014-09-25 19:46:07 36 | Max Avg. Temperature: 73.3124163116 F, 2014-09-28 00:00:00 37 | Min Avg. Temperature: 72.1644907648 F, 2014-09-30 00:00:00 38 | 39 | **** South **** 40 | Total Cooling Time (past 30 days): 6:02:15 41 | Total Heating Time (past 30 days): 0:00:00 42 | Total Off Time (past 30 days): 2 days, 1:37:30 43 | kW for cooling: 5.4542680359 44 | ================== 45 | Max Inst. Temperature : 214748364.7 F, 2014-09-26 23:58:04 46 | Min Inst. Temperature: -214748364.7 F, 2014-09-25 21:17:58 47 | Max Avg. Temperature: -103711.277503 F, 2014-09-29 00:00:00 48 | Min Avg. Temperature: -1143607.9526 F, 2014-09-26 00:00:00 49 | 50 | **** Server **** 51 | Total Cooling Time (past 30 days): 22:16:55 52 | Total Heating Time (past 30 days): 0:00:00 53 | Total Off Time (past 30 days): 2 days, 11:19:40 54 | kW for cooling: 3.8184901401 55 | ================== 56 | Max Inst. Temperature : 214748364.7 F, 2014-09-29 18:27:07 57 | Min Inst. Temperature: -214748364.7 F, 2014-09-29 18:25:55 58 | Max Avg. Temperature: 72.0344349347 F, 2014-09-28 00:00:00 59 | Min Avg. Temperature: -137469.736315 F, 2014-09-30 00:00:00 60 | -------------------------------------------------------------------------------- /monitor/report/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, url_for, jsonify 2 | import json 3 | 4 | def fix_name(name): 5 | return name.replace(" ","_") 6 | 7 | data = json.load(open('report.json')) 8 | app = Flask(__name__) 9 | app.add_template_global(fix_name) 10 | 11 | @app.route("/") 12 | def report(): 13 | total_demand = data['demand']['Total Demand'] 14 | disaggregation_results = data['disaggregate'] 15 | reportdata = {k:v for k,v in data['demand'].iteritems() if k not in ["Demand Data", "Total Demand"]} 16 | return render_template("index.html", total_demand=total_demand, demand=reportdata, disaggregation_results=disaggregation_results, zones=data['zones']) 17 | 18 | @app.route("/demanddata") 19 | def demanddata(): 20 | return jsonify({'data': [{'date': k,'value': v} for k,v in data['demand']['Demand Data'].iteritems()]}) 21 | 22 | @app.route("/zonedata//") 23 | def zonedata(key, zone): 24 | """ 25 | key: "Min Daily Avg Demand" from report.demand_report 26 | zone: zone name like "DOSA" 27 | """ 28 | key = key.replace("_"," ") 29 | ret = {} 30 | for ts, dd in data['demand'][key]['Data'][zone].iteritems(): 31 | ret[ts] = [{'date': k, 'value': v} for k,v in dd.iteritems()] 32 | return jsonify(ret) 33 | 34 | @app.route("/histogram/") 35 | def histogram(zone): 36 | return jsonify({'data': data['disaggregate_histograms'][zone]}) 37 | 38 | @app.route("/hvacdemand") 39 | def hvacdemand(): 40 | return jsonify(data['hvac_demand']) 41 | 42 | if __name__=='__main__': 43 | app.run(host="0.0.0.0",debug=True) 44 | -------------------------------------------------------------------------------- /monitor/report/static/debut-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/monitor/report/static/debut-light.png -------------------------------------------------------------------------------- /monitor/report/static/report.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 200; 4 | font-size: 150%; 5 | padding: 0; 6 | background-image: url('debut-light.png'); 7 | background-repeat: repeat-repeat; 8 | -webkit-user-select: none; 9 | -khtml-user-select: none; 10 | -moz-user-select: none; 11 | -o-user-select: none; 12 | user-select: none; 13 | } 14 | 15 | .center { 16 | text-align: center; 17 | } 18 | 19 | .col { 20 | padding-left: 50px; 21 | padding-right: 50px; 22 | } 23 | 24 | .plot-well { 25 | min-height: 20px; 26 | padding: 10px; 27 | margin-bottom: 10px; 28 | background-color: #fff; 29 | border: 1px solid #e3e3e3; 30 | border-radius: 4px; 31 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); 32 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); 33 | } 34 | 35 | 36 | .limit { 37 | max-width: 2000px; 38 | min-width: 2000px; 39 | padding: 20px; 40 | margin: 20px; 41 | } 42 | 43 | path { 44 | stroke: steelblue; 45 | stroke-width: 1; 46 | fill: none; 47 | } 48 | 49 | .temp { 50 | fill: none; 51 | stroke: black; 52 | } 53 | 54 | .temp_heat { 55 | fill: none; 56 | stroke: red; 57 | } 58 | 59 | .temp_cool { 60 | fill: none; 61 | stroke: blue; 62 | } 63 | 64 | .x.axis line, .x.axis path, .y.axis line, .y.axis path { 65 | fill: none; 66 | stroke: #000; 67 | } 68 | 69 | .bar rect { 70 | fill: lightcoral; 71 | shape-rendering: crispEdges; 72 | } 73 | 74 | .cleanwell { 75 | background-color: #fff; 76 | padding: 50px; 77 | } 78 | -------------------------------------------------------------------------------- /monitor/report/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Building Report 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |

Building Report

20 | 21 |
22 |

Demand

23 |

Total Demand

24 |

{{ total_demand['Amount'] }} kW : {{ total_demand['Date'] }}

25 |
26 |
27 | {% for zone in zones %} 28 |
29 |
30 |

{{ zone }} : {{ disaggregation_results[zone] }} kW

31 |
32 |
33 |
34 | {% endfor %} 35 | {% for k,v in demand.iteritems() %} 36 |

{{ k }}

37 |

{{ v['Amount'] }} kW : {{ v['Date'] }}

38 |
39 |
40 | {% if v.has_key('Data') %} 41 | {% for zone, plotnames in v['Data'].iteritems() %} 42 |
43 |
44 |
45 | {% endfor %} 46 | {% endif %} 47 |
48 |
49 | {% endfor %} 50 |
51 |
52 |
53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /openbas/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | -------------------------------------------------------------------------------- /openbas/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /openbas/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 2hcmqjoy6hmr10sulzx 8 | -------------------------------------------------------------------------------- /openbas/.meteor/cordova-plugins: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /openbas/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | standard-app-packages 6 | autopublish 7 | insecure 8 | underscore 9 | accounts-base 10 | accounts-password 11 | http 12 | jquery 13 | kidovate:bootstrap-slider 14 | mrt:moment 15 | s3ui 16 | anytime 17 | vakata-jstree 18 | timezone-js 19 | jquery-simplecolorpicker 20 | newd3 21 | iron:router 22 | mizzao:jquery-ui 23 | mizzao:bootstrap-3 24 | cfs:standard-packages 25 | cfs:filesystem 26 | ian:accounts-ui-bootstrap-3 27 | 28 | -------------------------------------------------------------------------------- /openbas/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@0.9.3.1 2 | -------------------------------------------------------------------------------- /openbas/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.1.1 2 | accounts-password@1.0.2 3 | anti:i18n@0.4.3 4 | anytime@0.0.0 5 | application-configuration@1.0.2 6 | autopublish@1.0.0 7 | autoupdate@1.1.1 8 | base64@1.0.0 9 | binary-heap@1.0.0 10 | blaze-tools@1.0.0 11 | blaze@2.0.1 12 | boilerplate-generator@1.0.0 13 | callback-hook@1.0.0 14 | cfs:access-point@0.0.0 15 | cfs:base-package@0.0.0 16 | cfs:collection-filters@0.0.0 17 | cfs:collection@0.0.0 18 | cfs:data-man@0.0.0 19 | cfs:file@0.0.0 20 | cfs:filesystem@0.0.0 21 | cfs:http-methods@0.0.24 22 | cfs:http-publish@0.0.0 23 | cfs:power-queue@0.0.1 24 | cfs:reactive-list@0.0.0 25 | cfs:reactive-property@0.0.0 26 | cfs:standard-packages@0.0.2 27 | cfs:storage-adapter@0.0.0 28 | cfs:tempstore@0.0.2 29 | cfs:upload-http@0.0.2 30 | cfs:worker@0.0.0 31 | check@1.0.1 32 | col-resizable@0.0.0 33 | ctl-helper@1.0.3 34 | ctl@1.0.1 35 | ddp@1.0.9 36 | deps@1.0.4 37 | ejson@1.0.3 38 | email@1.0.3 39 | fastclick@1.0.0 40 | follower-livedata@1.0.1 41 | geojson-utils@1.0.0 42 | handlebars@1.0.0 43 | html-tools@1.0.1 44 | htmljs@1.0.1 45 | http@1.0.6 46 | ian:accounts-ui-bootstrap-3@1.1.11 47 | id-map@1.0.0 48 | insecure@1.0.0 49 | iron:core@0.3.4 50 | iron:dynamic-template@0.4.1 51 | iron:layout@0.4.1 52 | iron:router@0.9.4 53 | jquery-migrate@0.0.0 54 | jquery-simplecolorpicker@0.0.0 55 | jquery@1.0.0 56 | json@1.0.0 57 | kidovate:bootstrap-slider@0.0.5 58 | livedata@1.0.10 59 | localstorage@1.0.0 60 | logging@1.0.3 61 | meteor-platform@1.1.1 62 | meteor@1.1.1 63 | minifiers@1.1.0 64 | minimongo@1.0.3 65 | mizzao:bootstrap-3@3.2.0_1 66 | mizzao:build-fetcher@0.2.0 67 | mizzao:jquery-ui@1.11.0 68 | mobile-status-bar@1.0.0 69 | mongo-livedata@1.0.5 70 | mongo@1.0.6 71 | mrt:moment@2.8.1 72 | newd3@0.0.0 73 | npm-bcrypt@0.7.7 74 | observe-sequence@1.0.2 75 | ordered-dict@1.0.0 76 | raix:eventemitter@0.0.1 77 | random@1.0.0 78 | reactive-dict@1.0.3 79 | reactive-var@1.0.2 80 | reload@1.1.0 81 | retry@1.0.0 82 | routepolicy@1.0.1 83 | s3ui@0.0.0 84 | service-configuration@1.0.1 85 | session@1.0.2 86 | sha@1.0.0 87 | spacebars-compiler@1.0.2 88 | spacebars@1.0.2 89 | srp@1.0.0 90 | standard-app-packages@1.0.2 91 | stylus@1.0.4 92 | templating@1.0.7 93 | timezone-js@0.0.0 94 | tracker@1.0.2 95 | ui@1.0.3 96 | underscore@1.0.0 97 | url@1.0.0 98 | vakata-jstree@0.0.0 99 | webapp-hashing@1.0.0 100 | webapp@1.1.2 101 | -------------------------------------------------------------------------------- /openbas/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Regents of the University of California 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /openbas/building.ini: -------------------------------------------------------------------------------- 1 | [report 0] 2 | ReportDeliveryLocation = mongo://localhost:3001 3 | 4 | [report 1] 5 | ReportDeliveryLocation = http://localhost:8079/add/lVzBMDpnkXApJmpjUDSvm4ceGfpbrLLSd9cq 6 | 7 | [report 2] 8 | ReportDeliveryLocation = http://archiver.cal-sdb.org:9000/data/legacyadd/likeaboss 9 | 10 | [/] 11 | uuid = 06d634a6-1c03-11e4-965d-6003089ed1d0 12 | Metadata/SourceName = Demo Driver 13 | Metadata/Building = Soda Hall 14 | Metadata/Site = 0273d18f-1c03-11e4-a490-6003089ed1d0 15 | Metadata/configured = True 16 | 17 | [server] 18 | port = 8081 19 | 20 | [/buildinglighting] 21 | type = Collection 22 | Metadata/System = Lighting 23 | Metadata/Role = Building Lighting 24 | Metadata/Floor = 4 25 | Metadata/Name = 4th Floor Lighting 26 | 27 | [/buildinglighting/group0] 28 | type = smap.drivers.lights.virtuallight.VirtualLight 29 | Metadata/Group = 0 30 | Metadata/LightingZone = 410 Soda 31 | 32 | [/buildinglighting/group1] 33 | type = smap.drivers.lights.virtuallight.VirtualLight 34 | Metadata/Group = 1 35 | Metadata/LightingZone = 410 Soda 36 | 37 | [/buildinglighting/group2] 38 | type = smap.drivers.lights.virtuallight.VirtualLight 39 | Metadata/Group = 2 40 | Metadata/LightingZone = 420 Soda 41 | 42 | [/buildinghvac] 43 | type = Collection 44 | Metadata/System = HVAC 45 | Metadata/Role = Building HVAC 46 | Metadata/Floor = 4 47 | Metadata/Name = 4th Floor HVAC 48 | 49 | [/buildinghvac/thermostat0] 50 | type = smap.drivers.thermostats.virtualthermostat.VirtualThermostat 51 | Metadata/Room = 410 Soda 52 | Metadata/HVACZone = 410 Soda W 53 | archiver=http://localhost:8079 54 | temp_heat=Metadata/HVACZone = '410 Soda W' and Metadata/Description = 'Zone Heating setpoint' 55 | temp_cool=Metadata/HVACZone = '410 Soda W' and Metadata/Description = 'Zone Cooling setpoint' 56 | 57 | [/buildinghvac/thermostat1] 58 | type = smap.drivers.thermostats.virtualthermostat.VirtualThermostat 59 | Metadata/Room = 410 Soda 60 | Metadata/HVACZone = 410 Soda E 61 | temp_heat=Metadata/HVACZone = '410 Soda E' and Metadata/Description = 'Zone Heating setpoint' 62 | temp_cool=Metadata/HVACZone = '410 Soda E' and Metadata/Description = 'Zone Cooling setpoint' 63 | 64 | [/buildinghvac/thermostat2] 65 | type = smap.drivers.thermostats.virtualthermostat.VirtualThermostat 66 | Metadata/Room = 411 Soda 67 | Metadata/HVACZone = 411 Soda 68 | temp_heat=Metadata/HVACZone = '411 Soda' and Metadata/Description = 'Zone Heating setpoint' 69 | temp_cool=Metadata/HVACZone = '411 Soda' and Metadata/Description = 'Zone Cooling setpoint' 70 | 71 | [/monitoring] 72 | type = Collection 73 | Metadata/System = Monitoring 74 | Metadata/Role = Monitoring 75 | Metadata/Floor = 4 76 | Metadata/Name = 4th Floor Monitoring 77 | 78 | [/monitoring/airtemphumidity0] 79 | type = smap.drivers.sensors.virtualATHsensor.VirtualATHSensor 80 | initialtemp = 68 81 | initialhumidity = 38 82 | Metadata/Type = Sensor 83 | Metadata/Room = 410 Soda 84 | Metadata/HVACZone = 410 Soda E 85 | 86 | [/monitoring/airtemphumidity1] 87 | type = smap.drivers.sensors.virtualATHsensor.VirtualATHSensor 88 | initialtemp = 69 89 | initialhumidity = 39 90 | Metadata/Type = Sensor 91 | Metadata/Room = 410 Soda 92 | Metadata/HVACZone = 410 Soda W 93 | 94 | [/monitoring/airtemphumidity2] 95 | type = smap.drivers.sensors.virtualATHsensor.VirtualATHSensor 96 | initialtemp = 70 97 | initialhumidity = 40 98 | Metadata/Type = Sensor 99 | Metadata/Room = 410 Soda 100 | Metadata/HVACZone = 410 Soda W 101 | 102 | [/monitoring/airtemphumidity3] 103 | type = smap.drivers.sensors.virtualATHsensor.VirtualATHSensor 104 | initialtemp = 71 105 | initialhumidity = 41 106 | Metadata/Type = Sensor 107 | Metadata/Room = 411 Soda 108 | Metadata/HVACZone = 411 Soda 109 | 110 | [/monitoring/illumination0] 111 | type = smap.drivers.sensors.virtualLightsensor.VirtualLightSensor 112 | initiallux = 140 113 | Metadata/Type = Sensor 114 | Metadata/Room = 410 Soda 115 | Metadata/LightingZone = 410 Soda 116 | 117 | [/monitoring/illumination1] 118 | type = smap.drivers.sensors.virtualLightsensor.VirtualLightSensor 119 | initiallux = 100 120 | Metadata/Type = Sensor 121 | Metadata/Room = 410 Soda 122 | Metadata/LightingZone = 410 Soda 123 | 124 | [/monitoring/illumination2] 125 | type = smap.drivers.sensors.virtualLightsensor.VirtualLightSensor 126 | initiallux = 300 127 | Metadata/Type = Sensor 128 | Metadata/Room = 420 Soda 129 | Metadata/LightingZone = 420 Soda 130 | 131 | [/monitoring/illumination3] 132 | type = smap.drivers.sensors.virtualLightsensor.VirtualLightSensor 133 | initiallux = 200 134 | Metadata/Type = Sensor 135 | Metadata/Room = 420 Soda 136 | Metadata/LightingZone = 420 Soda 137 | 138 | [/monitoring/demand0] 139 | type = smap.drivers.sensors.virtualpowermeter.VirtualPowerMeter 140 | Metadata/Type = Sensor 141 | Metadata/Room = Soda Hall 142 | 143 | [/tasklighting] 144 | type = Collection 145 | Metadata/System = Lighting 146 | Metadata/Role = Task Lighting 147 | 148 | [/tasklighting/group0] 149 | type = smap.drivers.lights.virtuallight.VirtualLight 150 | Metadata/Type = Command 151 | Metadata/Group = 0 152 | Metadata/LightingZone = 410 Soda 153 | 154 | [/tasklighting/group1] 155 | type = smap.drivers.lights.virtuallight.VirtualLight 156 | Metadata/Type = Command 157 | Metadata/Group = 1 158 | Metadata/LightingZone = 410 Soda 159 | 160 | [/tasklighting/group2] 161 | type = smap.drivers.lights.virtuallight.VirtualLight 162 | Metadata/Type = Command 163 | Metadata/Group = 2 164 | Metadata/LightingZone = 420 Soda 165 | 166 | ## General Control ## 167 | [/generalcontrol] 168 | type = Collection 169 | Metadata/System = GeneralControl 170 | 171 | [/generalcontrol/controller1] 172 | type = smap.drivers.virtualcontroller.VirtualController 173 | Metadata/HVACZone = 410 Soda W 174 | Metadata/Room = 410 Soda 175 | Metadata/Name = Virtual Space Heater 176 | 177 | [/generalcontrol/controller2] 178 | type = smap.drivers.virtualcontroller.VirtualController 179 | Metadata/HVACZone = 410 Soda W 180 | Metadata/Room = 410 Soda 181 | Metadata/Name = Virtual Fan 182 | 183 | ## Zone Controller ## 184 | [/zonecontroller1] 185 | type = followcontroller.FollowMaster 186 | subscribe/temp_heat = Metadata/Description = 'Master Heating setpoint' 187 | subscribe/temp_cool = Metadata/Description = 'Master Cooling setpoint' 188 | archiver = http://localhost:8079 189 | synchronous = False 190 | Metadata/HVACZone = 410 Soda W 191 | 192 | [/zonecontroller1/temp_heat] 193 | Metadata/Description = Zone Heating setpoint 194 | Metadata/Type = Setpoint 195 | 196 | [/zonecontroller1/temp_cool] 197 | Metadata/Description = Zone Cooling setpoint 198 | Mnetadata/Type = Setpoint 199 | 200 | [/zonecontroller2] 201 | type = followcontroller.FollowMaster 202 | subscribe/temp_heat = Metadata/Description = 'Master Heating setpoint' 203 | subscribe/temp_cool = Metadata/Description = 'Master Cooling setpoint' 204 | archiver = http://localhost:8079 205 | synchronous = False 206 | Metadata/HVACZone = 410 Soda E 207 | 208 | [/zonecontroller2/temp_heat] 209 | Metadata/Description = Zone Heating setpoint 210 | Metadata/Type = Setpoint 211 | 212 | [/zonecontroller2/temp_cool] 213 | Metadata/Description = Zone Cooling setpoint 214 | Mnetadata/Type = Setpoint 215 | 216 | [/zonecontroller3] 217 | type = followcontroller.FollowMaster 218 | subscribe/temp_heat = Metadata/Description = 'Master Heating setpoint' 219 | subscribe/temp_cool = Metadata/Description = 'Master Cooling setpoint' 220 | archiver = http://localhost:8079 221 | synchronous = False 222 | Metadata/HVACZone = 411 Soda 223 | 224 | [/zonecontroller3/temp_heat] 225 | Metadata/Description = Zone Heating setpoint 226 | Metadata/Type = Setpoint 227 | 228 | [/zonecontroller3/temp_cool] 229 | Metadata/Description = Zone Cooling setpoint 230 | Mnetadata/Type = Setpoint 231 | 232 | ## Master Scheduler ## 233 | [/masterscheduler] 234 | type = smap.services.scheduler.Scheduler 235 | rate = 1 236 | source = mongodb://localhost:3001/meteor 237 | 238 | [/masterscheduler/temp_heat] 239 | Metadata/Description = Master Heating setpoint 240 | Metadata/System = Schedule 241 | Metadata/Type = Setpoint 242 | 243 | [/masterscheduler/temp_cool] 244 | Metadata/Description = Master Cooling setpoint 245 | Metadata/System = Schedule 246 | Metadata/Type = Setpoint 247 | 248 | [/masterscheduler/on] 249 | Metadata/Description = Master Lighting control 250 | Metadata/System = Schedule 251 | Metadata/Type = Command 252 | -------------------------------------------------------------------------------- /openbas/client/building.html: -------------------------------------------------------------------------------- 1 | 73 | 74 | 77 | 78 | 148 | 149 | 161 | 162 | 194 | 195 | -------------------------------------------------------------------------------- /openbas/client/building.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Regents of the University of California 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, this 8 | // list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | Template.add_room.rendered = function() { 26 | var rooms = Rooms.find({}).fetch(); 27 | var hvac_zones = _.pluck(rooms, 'HVACZone'); 28 | var descriptions = _.pluck(rooms, 'Description'); 29 | var lighting_zones = _.pluck(rooms, 'LightingZones'); 30 | 31 | $('#room-description').autocomplete({ 32 | source: _.uniq(descriptions), 33 | minLength: 0, 34 | }); 35 | 36 | $('#hvac-zone').autocomplete({ 37 | source: _.uniq(hvac_zones), 38 | minLength: 0, 39 | }); 40 | 41 | $('#lighting-zone').autocomplete({ 42 | source: _.uniq(lighting_zones), 43 | minLength: 0, 44 | }); 45 | }; 46 | 47 | Template.add_room.events({ 48 | 'click #save-room': function(){ 49 | var marker = $(".floorplan-marker"); 50 | if (marker.length){ 51 | var image = marker.siblings('img'); 52 | var floorplan_id = image.attr('id'); 53 | var img_position_abs = image.position(); 54 | var marker_position_abs = marker.position(); 55 | var marker_position_rel = { 56 | 'top': marker_position_abs.top - img_position_abs.top, 57 | 'left': marker_position_abs.left - img_position_abs.left 58 | }; 59 | } else { 60 | var marker_position_rel = null; 61 | } 62 | var r = { 63 | 'RoomNumber': $("#room-number").val(), 64 | 'Description': $("#room-description").val(), 65 | 'HVACZone': $("#hvac-zone").val(), 66 | 'LightingZone': $("#lighting-zone").val(), 67 | 'Exposure': $("#exposure").val(), 68 | 'FloorplanId': floorplan_id, 69 | 'MarkerPosition': marker_position_rel, 70 | }; 71 | if (this._id != undefined){ 72 | Rooms.update({_id: this._id}, r); 73 | } else { 74 | Rooms.insert(r); 75 | } 76 | Router.go('/building/'); 77 | }, 78 | 'click #cancel-room': function(){ 79 | Router.go('/building/'); 80 | }, 81 | 'click .floorplan': function(event){ 82 | $('.floorplan-marker').remove(); 83 | var offsetX = -15; 84 | var offsetY = -31; 85 | var markerX = event.pageX + offsetX; 86 | var markerY = event.pageY + offsetY; 87 | var marker = $('') 88 | .attr('class', 'floorplan-marker glyphicon glyphicon-map-marker') 89 | .attr('display', 'none') 90 | .css('left', markerX + "px") 91 | .css('top', markerY + "px"); 92 | 93 | $(event.target).parent('div').append(marker); 94 | 95 | jQuery({count: 100}).animate({count: 0},{ 96 | duration: 1000, 97 | step: function(){ 98 | marker.css('top', markerY - this.count) 99 | }, 100 | easing: 'easeOutBounce', 101 | }); 102 | }, 103 | }); 104 | 105 | Template.building.events({ 106 | 'click #upload-floorplan': function(event, template) { 107 | $("#loading-gif").show(); 108 | var file = $('#floorplan-file')[0].files[0]; 109 | var description = $('#floorplan-description').val(); 110 | FloorplansFS.insert(file, function (err, fileObj) { 111 | Floorplans.insert({"description": description, "file_id": fileObj._id}); 112 | }); 113 | Router.go('/building'); 114 | }, 115 | 'hover .floorplan-marker': function(event){ 116 | var room_id = $(event.target).data('room'); 117 | var room = Rooms.find({_id: room_id}).fetch(); 118 | }, 119 | 'click .floorplan-delete': function(event){ 120 | var floorplan_id = $(event.target).data('floorplan'); 121 | var modal = $('.modal') 122 | modal.modal('show'); 123 | $('.modal #confirm').click(function(){ 124 | Floorplans.remove({_id: floorplan_id}); 125 | $('#floorplan-'+floorplan_id).fadeOut('slow'); 126 | modal.modal('hide'); 127 | }); 128 | } 129 | }); 130 | 131 | function place_marker(room){ 132 | if (room.hasOwnProperty('MarkerPosition')){ 133 | var img_pos = $('img#' + room.FloorplanId).position(); 134 | var marker = $('') 135 | .attr('title', room.RoomNumber) 136 | .attr('class', 'floorplan-marker floorplan-marker-static glyphicon glyphicon-map-marker') 137 | .attr('data-room', room._id) 138 | .css('left', img_pos.left + room.MarkerPosition.left + "px") 139 | .css('top', img_pos.top + room.MarkerPosition.top + "px"); 140 | $('div#floorplan-' + room.FloorplanId).append(marker); 141 | 142 | marker.click(function(){ 143 | Router.go('/room/' + room.RoomNumber); 144 | }); 145 | } 146 | } 147 | 148 | function draw_markers() { 149 | var rooms = Rooms.find({}).fetch(); 150 | _.each(rooms, function(room){ 151 | place_marker(room); 152 | }); 153 | $(".floorplan-marker").tooltip({ 154 | placement: "top", 155 | }); 156 | } 157 | 158 | Template.building.rendered = function() { 159 | draw_markers(); 160 | // make sure all images are loaded 161 | $(window).load(function(){ 162 | draw_markers(); 163 | }); 164 | $(window).resize(function(){ 165 | $('.floorplan-marker').remove(); 166 | draw_markers(); 167 | }); 168 | }; 169 | 170 | Template.building.floorplans = function() { 171 | return Floorplans.find({}, {reactive: false}); 172 | }; 173 | Template.add_room.floorplans = Template.building.floorplans; 174 | 175 | Template.building.rooms = function() { 176 | return Rooms.find({}); 177 | }; 178 | 179 | Template.floorplan.helpers({ 180 | getImgPath: function(){ 181 | var fpfile = FloorplansFS.findOne({'_id': this.file_id}); 182 | if (fpfile.hasCopy('images')){ 183 | return '/floorplans/' + fpfile.copies.images.key; 184 | } else { 185 | return '/img/ajax-loader.gif'; 186 | } 187 | }, 188 | }); 189 | 190 | Template.edit_room.rendered = function() { 191 | $('.add-room-container').find('.panel-heading').html('Edit room'); 192 | $('#room-number').val(this.data.RoomNumber); 193 | $('#room-description').val(this.data.Description); 194 | $('#hvac-zone').val(this.data.HVACZone); 195 | $('#lighting-zone').val(this.data.LightingZone); 196 | $('#exposure').val(this.data.Exposure); 197 | place_marker(this.data); 198 | }; 199 | -------------------------------------------------------------------------------- /openbas/client/compatibility/buttons.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Project: Buttons 3 | * Description: A highly customizable CSS button library built with Sass and Compass 4 | * Author: Alex Wolfe 5 | * License: Apache License v2.0 6 | */ 7 | 8 | // the semi-colon before function invocation is a safety net against concatenated 9 | // scripts and/or other plugins which may not be closed properly. 10 | ;(function ( $, window, document, undefined ) { 11 | 'use strict'; 12 | 13 | // undefined is used here as the undefined global variable in ECMAScript 3 is 14 | // mutable (ie. it can be changed by someone else). undefined isn't really being 15 | // passed in so we can ensure the value of it is truly undefined. In ES5, undefined 16 | // can no longer be modified. 17 | 18 | // window and document are passed through as local variable rather than global 19 | // as this (slightly) quickens the resolution process and can be more efficiently 20 | // minified (especially when both are regularly referenced in your plugin). 21 | 22 | // Create the defaults once 23 | var pluginName = "menuButton"; 24 | var menuClass = ".button-dropdown"; 25 | var defaults = { 26 | propertyName: "value" 27 | }; 28 | 29 | // The actual plugin constructor 30 | function Plugin( element, options ) { 31 | 32 | //SET OPTIONS 33 | this.options = $.extend( {}, defaults, options ); 34 | this._defaults = defaults; 35 | this._name = pluginName; 36 | 37 | //REGISTER ELEMENT 38 | this.$element = $(element); 39 | 40 | //INITIALIZE 41 | this.init(); 42 | } 43 | 44 | Plugin.prototype = { 45 | constructor: Plugin, 46 | 47 | init: function() { 48 | // WE DON'T STOP PROPGATION SO CLICKS WILL AUTOMATICALLY 49 | // TOGGLE AND REMOVE THE DROPDOWN & OVERLAY 50 | this.toggle(); 51 | }, 52 | 53 | toggle: function(el, options) { 54 | if(this.$element.data('dropdown') === 'show') { 55 | this.hideMenu(); 56 | } 57 | else { 58 | this.showMenu(); 59 | } 60 | }, 61 | 62 | showMenu: function() { 63 | this.$element.data('dropdown', 'show'); 64 | this.$element.find('ul').show(); 65 | 66 | if(this.$overlay) { 67 | this.$overlay.show(); 68 | } 69 | else { 70 | this.$overlay = $('
'); 71 | this.$element.append(this.$overlay); 72 | } 73 | }, 74 | 75 | hideMenu: function() { 76 | this.$element.data('dropdown', 'hide'); 77 | this.$element.find('ul').hide(); 78 | this.$overlay.hide(); 79 | } 80 | }; 81 | 82 | // A really lightweight plugin wrapper around the constructor, 83 | // preventing against multiple instantiations 84 | $.fn[pluginName] = function ( options ) { 85 | return this.each(function () { 86 | 87 | // TOGGLE BUTTON IF IT EXISTS 88 | if ($.data(this, "plugin_" + pluginName)) { 89 | $.data(this, "plugin_" + pluginName).toggle(); 90 | } 91 | // OTHERWISE CREATE A NEW INSTANCE 92 | else { 93 | $.data(this, "plugin_" + pluginName, new Plugin( this, options )); 94 | } 95 | }); 96 | }; 97 | 98 | 99 | //DELEGATE CLICK EVENT FOR DROPDOWN MENUS 100 | $(document).on('click', '[data-buttons=dropdown]', function(e) { 101 | var $dropdown = $(e.currentTarget); 102 | $dropdown.menuButton(); 103 | }); 104 | 105 | //IGNORE CLICK EVENTS FROM DISPLAY BUTTON IN DROPDOWN 106 | $(document).on('click', '[data-buttons=dropdown] > a', function(e) { 107 | e.preventDefault(); 108 | }); 109 | 110 | })( jQuery, window, document); -------------------------------------------------------------------------------- /openbas/client/css/slider.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Slider for Bootstrap 3 | * 4 | * Copyright 2012 Stefan Petre 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | */ 9 | .slider { 10 | display: inline-block; 11 | vertical-align: middle; 12 | position: relative; 13 | } 14 | .slider.slider-horizontal { 15 | width: 210px; 16 | height: 20px; 17 | } 18 | .slider.slider-horizontal .slider-track { 19 | height: 10px; 20 | width: 100%; 21 | margin-top: -5px; 22 | top: 50%; 23 | left: 0; 24 | } 25 | .slider.slider-horizontal .slider-selection { 26 | height: 100%; 27 | top: 0; 28 | bottom: 0; 29 | } 30 | .slider.slider-horizontal .slider-handle { 31 | margin-left: -10px; 32 | margin-top: -5px; 33 | } 34 | .slider.slider-horizontal .slider-handle.triangle { 35 | border-width: 0 10px 10px 10px; 36 | width: 0; 37 | height: 0; 38 | border-bottom-color: #0480be; 39 | margin-top: 0; 40 | } 41 | .slider.slider-vertical { 42 | height: 210px; 43 | width: 20px; 44 | } 45 | .slider.slider-vertical .slider-track { 46 | width: 10px; 47 | height: 100%; 48 | margin-left: -5px; 49 | left: 50%; 50 | top: 0; 51 | } 52 | .slider.slider-vertical .slider-selection { 53 | width: 100%; 54 | left: 0; 55 | top: 0; 56 | bottom: 0; 57 | } 58 | .slider.slider-vertical .slider-handle { 59 | margin-left: -5px; 60 | margin-top: -10px; 61 | } 62 | .slider.slider-vertical .slider-handle.triangle { 63 | border-width: 10px 0 10px 10px; 64 | width: 1px; 65 | height: 1px; 66 | border-left-color: #0480be; 67 | margin-left: 0; 68 | } 69 | .slider input { 70 | display: none; 71 | } 72 | .slider .tooltip-inner { 73 | white-space: nowrap; 74 | } 75 | .slider-track { 76 | position: absolute; 77 | cursor: pointer; 78 | background-color: #f7f7f7; 79 | background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); 80 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); 81 | background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); 82 | background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); 83 | background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); 84 | background-repeat: repeat-x; 85 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); 86 | -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); 87 | -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); 88 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); 89 | -webkit-border-radius: 4px; 90 | -moz-border-radius: 4px; 91 | border-radius: 4px; 92 | } 93 | .slider-selection { 94 | position: absolute; 95 | background-color: #f7f7f7; 96 | background-image: -moz-linear-gradient(top, #f9f9f9, #f5f5f5); 97 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f9f9f9), to(#f5f5f5)); 98 | background-image: -webkit-linear-gradient(top, #f9f9f9, #f5f5f5); 99 | background-image: -o-linear-gradient(top, #f9f9f9, #f5f5f5); 100 | background-image: linear-gradient(to bottom, #f9f9f9, #f5f5f5); 101 | background-repeat: repeat-x; 102 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0); 103 | -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); 104 | -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); 105 | box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); 106 | -webkit-box-sizing: border-box; 107 | -moz-box-sizing: border-box; 108 | box-sizing: border-box; 109 | -webkit-border-radius: 4px; 110 | -moz-border-radius: 4px; 111 | border-radius: 4px; 112 | } 113 | .slider-handle { 114 | position: absolute; 115 | width: 20px; 116 | height: 20px; 117 | background-color: #0e90d2; 118 | background-image: -moz-linear-gradient(top, #149bdf, #0480be); 119 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); 120 | background-image: -webkit-linear-gradient(top, #149bdf, #0480be); 121 | background-image: -o-linear-gradient(top, #149bdf, #0480be); 122 | background-image: linear-gradient(to bottom, #149bdf, #0480be); 123 | background-repeat: repeat-x; 124 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); 125 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); 126 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); 127 | box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); 128 | opacity: 0.8; 129 | border: 0px solid transparent; 130 | } 131 | .slider-handle.round { 132 | -webkit-border-radius: 20px; 133 | -moz-border-radius: 20px; 134 | border-radius: 20px; 135 | } 136 | .slider-handle.triangle { 137 | background: transparent none; 138 | } -------------------------------------------------------------------------------- /openbas/client/dashboard.css: -------------------------------------------------------------------------------- 1 | .dashboard-column { 2 | padding: 5px; 3 | } 4 | 5 | h2.dashboard-column-heading { 6 | margin-top: 0px; 7 | margin-bottom: 5px; 8 | color: #939393; 9 | font-size: 1.5em; 10 | text-align: center; 11 | text-shadow: -1px 1px 2px #ccc; 12 | } 13 | 14 | h3.dashboard-column-heading { 15 | font-size: 1.2em; 16 | margin-top: 9px; 17 | } 18 | 19 | .dark { 20 | background: #f5f5f5; 21 | } 22 | 23 | .infobox { 24 | font-size: small; 25 | } 26 | 27 | .centertext { 28 | margin-left: auto; 29 | margin-right: auto; 30 | text-align: center; 31 | } 32 | 33 | .sensorbox { 34 | outline: 1px solid black; 35 | padding: 5px; 36 | } 37 | 38 | .dashboard-well { 39 | min-height: 20px; 40 | padding: 10px; 41 | margin-bottom: 10px; 42 | background-color: #fff; 43 | border: 1px solid #e3e3e3; 44 | border-radius: 4px; 45 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); 46 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); 47 | } 48 | 49 | .sparkline { 50 | fill: none; 51 | stroke: #000; 52 | stroke-width: 0.5px; 53 | } 54 | 55 | .sparkline-container { 56 | float: left; 57 | margin-right: 6px; 58 | } 59 | 60 | svg.HVAC-zone-summary { 61 | display: block; 62 | margin: 0 auto; 63 | } 64 | 65 | div.HVAC-zone-summary { 66 | margin-top: 10px; 67 | margin-bottom: 10px; 68 | } 69 | 70 | .heattempline { 71 | fill: none; 72 | stroke: #ff0000; 73 | stroke-width: 1px; 74 | } 75 | 76 | .cooltempline { 77 | fill: none; 78 | stroke: #0000ff; 79 | stroke-width: 1px; 80 | } 81 | 82 | .templine { 83 | fill: none; 84 | stroke: #000; 85 | stroke-width: 1px; 86 | } 87 | 88 | .table-heading { 89 | padding-top: 5px; 90 | padding-bottom: 5px; 91 | font-size: small; 92 | } 93 | 94 | .zone-detail-container { 95 | margin: 0 auto; 96 | width: 70%; 97 | max-width: 800px; 98 | } 99 | 100 | .demand-widget { 101 | height: 75px; 102 | } 103 | 104 | td.day { 105 | text-align: center; 106 | } 107 | 108 | td.current-day { 109 | font-weight: bold; 110 | } 111 | 112 | .current-period{ 113 | background-color: #FEFFF2; 114 | } 115 | 116 | .overlay { 117 | fill: none; 118 | pointer-events: all; 119 | } 120 | 121 | .focus text { 122 | font-size: 1.2em; 123 | } 124 | 125 | /* Chrome, Safari, Opera */ 126 | @-webkit-keyframes snowflake { 127 | 0% {color: blue;} 128 | 50% {color: #ADD8E6;} 129 | 100% {color: red;} 130 | } 131 | 132 | /* Standard syntax */ 133 | @keyframes snowflake { 134 | 0% {color: blue;} 135 | 30% {color: #ADD8E6;} 136 | 100% {color: blue;} 137 | } 138 | 139 | /* Chrome, Safari, Opera */ 140 | @-webkit-keyframes flame { 141 | 0% {color: red;} 142 | 50% {color: #FFA500;} 143 | 100% {color: red;} 144 | } 145 | 146 | /* Standard syntax */ 147 | @keyframes flame { 148 | 0% {color: red;} 149 | 30% {color: #FFA500;} 150 | 100% {color: red;} 151 | } 152 | 153 | span#snowflake { 154 | -webkit-animation-name: snowflake; 155 | -webkit-animation-duration: 1.5s; 156 | -webkit-animation-iteration-count: infinite; 157 | -webkit-animation-timing-function: linear; 158 | } 159 | 160 | @-webkit-keyframes snowflake { 161 | from { 162 | -webkit-transform: rotate( 0deg ); 163 | } 164 | to { 165 | -webkit-transform: rotate( 360deg ); 166 | } 167 | } 168 | 169 | span#flame { 170 | -webkit-animation: flame 4s linear infinite; /* Chrome, Safari, Opera */ 171 | animation: flame 4s linear infinite; 172 | } 173 | 174 | .HVAC-status-label { 175 | margin-left: 7px; 176 | } 177 | 178 | .HVAC-state-container { 179 | font-size: 1.2em; 180 | text-align: center; 181 | margin-top: 5px; 182 | margin-bottom: 10px; 183 | } 184 | -------------------------------------------------------------------------------- /openbas/client/openbas.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 200; 4 | font-size: 150%; 5 | padding: 0; 6 | background-image: url(img/debut-light.png); 7 | background-repeat: repeat-repeat; 8 | -webkit-user-select: none; 9 | -khtml-user-select: none; 10 | -moz-user-select: none; 11 | -o-user-select: none; 12 | user-select: none; 13 | } 14 | 15 | .point { 16 | padding: 5px; 17 | } 18 | 19 | .value { 20 | font-size: 1.25em; 21 | font-weight: bold; 22 | color: #777; 23 | } 24 | 25 | .point .actuator { 26 | display: inline-block; 27 | width: 100px; 28 | padding-left: 10px; 29 | text-align: right; 30 | font-size: 2em; 31 | font-weight: bold; 32 | color: #777; 33 | } 34 | 35 | #actuators { 36 | width: 900px; 37 | margin: 0 auto; 38 | } 39 | 40 | div.container { 41 | width: 900px; 42 | margin: 0 auto; 43 | } 44 | 45 | div.schedule-container { 46 | width: 500px; 47 | margin: 0 auto; 48 | } 49 | 50 | div.rooms-container { 51 | width: 600px; 52 | margin: 0 auto; 53 | } 54 | 55 | div.add-room-container { 56 | width: 400px; 57 | margin: 0 auto; 58 | } 59 | 60 | table td.text-label { 61 | font-weight: bold; 62 | } 63 | 64 | a:hover { 65 | text-decoration: none; 66 | } 67 | 68 | .ui-autocomplete { 69 | z-index: 5000; 70 | } 71 | 72 | .popover { 73 | z-index: 6000; 74 | } 75 | 76 | .floorplan-marker { 77 | position: absolute; 78 | font-size: 2em; 79 | color: #E00000; 80 | } 81 | 82 | .floorplan-marker-static:hover { 83 | color: red; 84 | } 85 | 86 | .modal { 87 | overflow-y:auto; 88 | } 89 | 90 | 91 | .atooltip { 92 | display: inline; 93 | position: relative; 94 | } 95 | 96 | .atooltip:hover { 97 | color: #c00; 98 | text-decoration: none; 99 | } 100 | 101 | .atooltip:hover:after { 102 | background: #111; 103 | background: rgba(0,0,0,.8); 104 | border-radius: .5em; 105 | bottom: 1.35em; 106 | color: #fff; 107 | content: attr(title); 108 | display: block; 109 | left: 1em; 110 | padding: .3em 1em; 111 | position: absolute; 112 | text-shadow: 0 1px 0 #000; 113 | white-space: nowrap; 114 | z-index: 98; 115 | } 116 | 117 | .atooltip:hover:before { 118 | border: solid; 119 | border-color: #111 transparent; 120 | border-color: rgba(0,0,0,.8) transparent; 121 | border-width: .4em .4em 0 .4em; 122 | bottom: 1em; 123 | content: ""; 124 | display: block; 125 | left: 2em; 126 | position: absolute; 127 | z-index: 99; 128 | } 129 | 130 | .hvac-zone-table tr { 131 | height: 51px; 132 | } 133 | 134 | .focus-sparkline text { 135 | font-size: 1.1em; 136 | opacity: 0.7; 137 | } 138 | 139 | span.glyph-animate { 140 | -webkit-animation-name: rotate; 141 | -webkit-animation-duration: 1.5s; 142 | -webkit-animation-iteration-count: infinite; 143 | -webkit-animation-timing-function: linear; 144 | } 145 | 146 | @-webkit-keyframes rotate { 147 | from { 148 | -webkit-transform: rotate( 0deg ); 149 | } 150 | to { 151 | -webkit-transform: rotate( 360deg ); 152 | } 153 | } 154 | 155 | .loading-div { 156 | color: #939393; 157 | font-size: 1.5em; 158 | text-align: center; 159 | margin: 0 auto; 160 | width: 50%; 161 | text-shadow: -1px 1px 2px #ccc; 162 | margin-top:50px; 163 | } 164 | -------------------------------------------------------------------------------- /openbas/client/openbas.html: -------------------------------------------------------------------------------- 1 | 2 | OpenBAS 3 | 4 | 5 | 6 | {{> navbar}} 7 | 8 | 9 | 42 | 43 | 50 | 51 | 72 | 73 | 82 | 83 | 101 | 102 | 117 | 118 | 125 | 126 | 130 | 131 | 135 | 136 | 140 | 141 | 147 | 148 | 191 | 192 | 197 | -------------------------------------------------------------------------------- /openbas/client/openbas.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Regents of the University of California 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, this 8 | // list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | Accounts.ui.config({ 26 | passwordSignupFields: 'USERNAME_ONLY' 27 | }); 28 | 29 | OpenBAS = {}; 30 | 31 | OpenBAS.PathNames = [ 32 | {"path": "temp_heat", "name": "Heating setpoint"}, 33 | {"path": "temp_cool", "name": "Cooling setpoint"}, 34 | {"path": "hvac_state", "name": "HVAC state"}, 35 | {"path": "on", "name": "Lights"}, 36 | ]; 37 | 38 | Template.points.pointsAll = function() { 39 | return Points.find({}); 40 | }; 41 | 42 | Template.points.helpers({ 43 | notActuator: function(template){ 44 | var re = /.*_act$/; 45 | var result = re.exec(this.Path); 46 | return result == null; 47 | } 48 | }); 49 | 50 | Template.point_row.value_fmt = function() { 51 | var val = this.value 52 | if ((String(val).split('.')[1] || []).length > 2){ 53 | return this.value.toFixed(2); 54 | } else { 55 | return this.value; 56 | } 57 | } 58 | 59 | Template.navbar.building_name = function() { 60 | return Meteor.settings.public.building_name; 61 | }; 62 | 63 | Template.navbar.helpers({ 64 | activeIf: function (template) { 65 | var currentRoute = Router.current(); 66 | return currentRoute && 67 | template === currentRoute.lookupTemplate() ? 'active' : ''; 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /openbas/client/plot.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /openbas/client/plot.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Regents of the University of California 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, this 8 | // list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | function parsePixelsToInt(q) { 26 | return parseFloat(q.slice(0, q.length - 2)); 27 | } 28 | instances = []; 29 | if (Meteor.isClient) { 30 | var localtest = false; 31 | Template.plot.plot_data = [ 32 | { 33 | tagsURL: localtest ? 'http://localhost:7856' : (Meteor.settings.public.archiverUrl + "/api/query?"), 34 | dataURLStart: localtest ? 'http://localhost:7856/data/uuid' : 'http://archiver.cal-sdb.org:9000/data/uuid/', 35 | bracketURL: "http://archiver.cal-sdb.org:9000/q/bracket", 36 | }, 37 | function (inst) 38 | { 39 | instances.push(inst); 40 | s3ui.default_cb1(inst); 41 | }, 42 | s3ui.default_cb2]; 43 | } 44 | -------------------------------------------------------------------------------- /openbas/client/point.css: -------------------------------------------------------------------------------- 1 | svg { 2 | font: 10px sans-serif; 3 | } 4 | 5 | .line { 6 | fill: none; 7 | stroke: #000; 8 | stroke-width: 1.5px; 9 | } 10 | 11 | .axis path, 12 | .axis line { 13 | fill: none; 14 | stroke: #000; 15 | shape-rendering: crispEdges; 16 | } 17 | 18 | .pointDetailContainer { 19 | width: 900px; 20 | margin: 0 auto; 21 | } 22 | -------------------------------------------------------------------------------- /openbas/client/point.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /openbas/client/point.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Regents of the University of California 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, this 8 | // list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | Template.points.rendered = function(){ 26 | var rows = $(".point"); 27 | _.each(rows, function(row){ 28 | var uuid = $(row).data('uuid'); 29 | var restrict = 'uuid="' + uuid + '"'; 30 | var elementId = '#trend-' + uuid; 31 | var width = 150; 32 | var height = 40; 33 | var N = width; 34 | var q = "select data in (now -8h, now) where " + restrict; 35 | Meteor.call("query", q, function(err, res){ 36 | if (res[0] != undefined){ 37 | var mydata = jsonify(res[0].Readings); 38 | sparkline(elementId, mydata, width, height, "last 8 hours"); 39 | } 40 | }); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /openbas/client/schedule.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Regents of the University of California 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, this 8 | // list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | var iperiod = 0; 26 | 27 | Template.schedule.master = function(){ 28 | return MasterSchedule.findOne({}); 29 | } 30 | 31 | Template.schedule.days = function(){ 32 | var rv = [ 33 | {'day': 'Sunday', 'day_id': 'sun'}, 34 | {'day': 'Monday', 'day_id': 'mon'}, 35 | {'day': 'Tuesday', 'day_id': 'tue'}, 36 | {'day': 'Wednesday', 'day_id': 'wed'}, 37 | {'day': 'Thursday', 'day_id': 'thu'}, 38 | {'day': 'Friday', 'day_id': 'fri'}, 39 | {'day': 'Saturday', 'day_id': 'sat'} 40 | ]; 41 | return rv; 42 | }; 43 | 44 | Template.schedule.schedules = function(){ 45 | return Schedules.find({}); 46 | }; 47 | 48 | Template.schedule.rendered = function(){ 49 | var master_sched = MasterSchedule.findOne({}); 50 | _.each(master_sched, function(type, day){ 51 | $('#'+day+'-schedule').val(type); 52 | }); 53 | }; 54 | 55 | Template.period_point.path_names = function(){ 56 | return OpenBAS.PathNames; 57 | }; 58 | 59 | Template.schedule.events({ 60 | 'click #save-schedule': function(){ 61 | var master_sched = MasterSchedule.findOne({}); 62 | var id = master_sched._id; 63 | _.each(master_sched, function(val, ind){ 64 | master_sched[ind] = $('#'+ind+'-schedule').val(); 65 | }); 66 | delete master_sched._id; 67 | var r = MasterSchedule.update(id, {$set: master_sched}); 68 | if (r){ 69 | $("#success-alert").slideDown(800); 70 | window.setTimeout(function(){ 71 | $("#success-alert").slideUp(800); 72 | }, 5000); 73 | } 74 | }, 75 | 'click .delete-schedule': function(event){ 76 | var clicked_id = $(event.target).data('id'); 77 | Schedules.remove({ _id: clicked_id}); 78 | }, 79 | }); 80 | 81 | Template.edit_schedule.events({ 82 | 'click #add-period': function(event){ 83 | iperiod++; 84 | var rendered = UI.renderWithData(Template.schedule_period, {'iperiod': iperiod}); 85 | UI.insert(rendered, $('table')[0]); 86 | }, 87 | 'click .add-control-point': function(event){ 88 | var clicked_name = $(event.target).data('name') || $(event.target).data('period'); 89 | var rendered = UI.render(Template.period_point); 90 | UI.insert(rendered, $("#schedule-period-" + clicked_name)[0]); 91 | }, 92 | 'click #edit-schedule-cancel': function(){ 93 | Router.go("/schedule"); 94 | }, 95 | 'click #edit-schedule-save': function(){ 96 | var id = this._id; 97 | var periods = _.map($('.schedule-period'), function(p){ 98 | rv = {}; 99 | rv.name = $(p).find(".period-name").val(); 100 | rv.start = $(p).find(".period-start").val(); 101 | rv.points = _.map($(p).find(".period-point"), function(point){ 102 | mypoint = {}; 103 | mypoint.path = $(point).find('.period-point-name').val(); 104 | mypoint.value = $(point).find('.period-point-value').val(); 105 | return mypoint; 106 | }); 107 | return rv; 108 | }); 109 | this.periods = periods; 110 | this.name = $('#schedule-name').val(); 111 | delete this._id; 112 | 113 | var r = Schedules.update(id, {$set: this}); 114 | if (r){ 115 | Router.go("/schedule/"); 116 | } 117 | }, 118 | 'click .remove-control-point': function(){ 119 | $(event.target).closest('tr').remove(); 120 | }, 121 | 'click .remove-period': function(){ 122 | var name = $(event.target).data('name'); 123 | $('#schedule-period-'+name).remove(); 124 | $(event.target).closest('tbody').remove(); 125 | } 126 | }); 127 | 128 | Template.edit_schedule.rendered = function(){ 129 | _.each(this.data.periods, function(p){ 130 | var el_period = $('#schedule-period-'+p.name); 131 | var el_period_point_name = $(el_period).find(".period-point-name"); 132 | var el_period_point_value = $(el_period).find(".period-point-value"); 133 | _.each(_.zip(el_period_point_name, el_period_point_value, p.points), function(x){ 134 | x[0].value = x[2].path; 135 | x[1].value = x[2].value; 136 | }); 137 | }); 138 | }; 139 | 140 | Template.add_schedule.events({ 141 | 'click #add-period': function(event){ 142 | iperiod++; 143 | var rendered = UI.renderWithData(Template.schedule_period, {'iperiod': iperiod}); 144 | UI.insert(rendered, $('table')[0]); 145 | }, 146 | 'click .add-control-point': function(event){ 147 | var clicked_iperiod = $(event.target).data('period'); 148 | var rendered = UI.render(Template.period_point); 149 | UI.insert(rendered, $("#schedule-period-" + clicked_iperiod)[0]); 150 | }, 151 | 'click #add-schedule-cancel': function(){ 152 | Router.go("/schedule/"); 153 | }, 154 | 'click #add-schedule-save': function(){ 155 | var sched = {}; 156 | var periods = _.map($('.schedule-period'), function(p){ 157 | rv = {}; 158 | rv.name = $(p).find(".period-name").val(); 159 | rv.start = $(p).find(".period-start").val(); 160 | rv.points = _.map($(p).find(".period-point"), function(point){ 161 | mypoint = {}; 162 | mypoint.path = $(point).find('.period-point-name').val(); 163 | mypoint.value = $(point).find('.period-point-value').val(); 164 | return mypoint; 165 | }); 166 | return rv; 167 | }); 168 | sched.periods = periods; 169 | sched.name = $('#schedule-name').val(); 170 | sched.color = "#EBCACA"; 171 | var r = Schedules.insert(sched); 172 | if (r){ 173 | Router.go("/schedule/"); 174 | } 175 | }, 176 | 'click .remove-control-point': function(){ 177 | $(event.target).closest('tr').remove(); 178 | }, 179 | 'click .remove-period': function(){ 180 | var period = $(event.target).data('period'); 181 | $('#schedule-period-'+period).remove(); 182 | $(event.target).closest('tbody').remove(); 183 | } 184 | }); 185 | 186 | Template.view_schedule.helpers({ 187 | getPathName: function(path){ 188 | var mypath = _.find(OpenBAS.PathNames, function(p){ 189 | return p.path == path; 190 | }); 191 | return mypath.name; 192 | }, 193 | }); 194 | -------------------------------------------------------------------------------- /openbas/client/status.html: -------------------------------------------------------------------------------- 1 | 42 | 43 | 53 | 54 | 98 | 99 | 104 | 105 | 107 | -------------------------------------------------------------------------------- /openbas/demo.ini: -------------------------------------------------------------------------------- 1 | [report 0] 2 | ReportDeliveryLocation = mongo://localhost:3001 3 | 4 | [report 1] 5 | ReportDeliveryLocation = http://new.openbms.org/backend/add/CFixR8pyZEWpk4bIzpeWI5CdDPWR1TlxANUW 6 | 7 | #[report 1] 8 | #ReportDeliveryLocation = new.openbms.. 9 | 10 | [/] 11 | uuid = db43b080-176c-11e4-b2ab-6003089ed1d0 12 | Metadata/SourceName = Demo Driver 13 | Metadata/Building = Soda Hall 14 | Metadata/Site = 2d1fd317-1772-11e4-87c5-6003089ed1d0 15 | 16 | [server] 17 | port = 8081 18 | 19 | [/buildinglighting] 20 | type = Collection 21 | Metadata/System = Lighting 22 | Metadata/Role = Building Lighting 23 | Metadata/Room = 410 Soda 24 | 25 | [/buildinglighting/group0] 26 | type = smap.drivers.lights.virtuallight.VirtualLight 27 | Metadata/Type = Light Group 28 | Metadata/Floor = 4 29 | Metadata/Group = 0 30 | Metadata/Name = 4th Floor Lighting 31 | 32 | [/buildinglighting/group1] 33 | type = smap.drivers.lights.virtuallight.VirtualLight 34 | Metadata/Type = Light Group 35 | Metadata/Floor = 5 36 | Metadata/Group = 1 37 | Metadata/Name = 5th Floor Lighting 38 | 39 | [/buildinglighting/group2] 40 | type = smap.drivers.lights.virtuallight.VirtualLight 41 | Metadata/Type = Light Group 42 | Metadata/Floor = 6 43 | Metadata/Group = 1 44 | Metadata/Name = 6th Floor Lighting 45 | 46 | [/buildinghvac] 47 | type = Collection 48 | Metadata/System = HVAC 49 | Metadata/Role = Building HVAC 50 | Metadata/Room = 410 Soda 51 | 52 | [/buildinghvac/group0] 53 | type = smap.drivers.thermostats.virtualthermostat.VirtualThermostat 54 | Metadata/Type = HVAC Group 55 | Metadata/Floor = 4 56 | Metadata/Group = 0 57 | Metadata/Name = 4th Floor HVAC 58 | 59 | [/buildinghvac/group1] 60 | type = smap.drivers.thermostats.virtualthermostat.VirtualThermostat 61 | Metadata/Type = HVAC Group 62 | Metadata/Floor = 5 63 | Metadata/Group = 0 64 | Metadata/Name = 5th Floor HVAC 65 | -------------------------------------------------------------------------------- /openbas/followcontroller.py: -------------------------------------------------------------------------------- 1 | from smap.services.zonecontroller import ZoneController 2 | 3 | class FollowMaster(ZoneController): 4 | def setup(self, opts): 5 | ZoneController.setup(self, opts) 6 | self.add_timeseries('/temp_heat','F',data_type='double') 7 | self.add_timeseries('/temp_cool','F',data_type='double') 8 | 9 | def step(self): 10 | print 'ZONE CONTROL',self.points 11 | self.add('/temp_heat', float(self.points['temp_heat'])) 12 | self.add('/temp_cool', float(self.points['temp_cool'])) 13 | 14 | class FollowMasterTrim(ZoneController): 15 | def setup(self, opts): 16 | ZoneController.setup(self, opts) 17 | self.add_timeseries('/temp_heat','F',data_type='double') 18 | self.add_timeseries('/temp_cool','F',data_type='double') 19 | 20 | def step(self): 21 | print 'ZONE CONTROL',self.points 22 | self.add('/temp_heat', float(self.points['temp_heat'])+5) 23 | self.add('/temp_cool', float(self.points['temp_cool'])+5) 24 | -------------------------------------------------------------------------------- /openbas/lib/collections.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Regents of the University of California 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, this 8 | // list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | Points = new Meteor.Collection("points"); 26 | Rooms = new Meteor.Collection("rooms"); 27 | HVAC = new Meteor.Collection("hvac"); 28 | Lighting = new Meteor.Collection("lighting"); 29 | Monitoring = new Meteor.Collection("monitoring"); 30 | GeneralControl = new Meteor.Collection("general_control"); 31 | Schedules = new Meteor.Collection("schedules"); 32 | MasterSchedule = new Meteor.Collection("master_schedule"); 33 | Unconfigured = new Meteor.Collection("unconfigured"); 34 | Floorplans = new Meteor.Collection("floorplans"); 35 | Site = new Meteor.Collection("site"); 36 | 37 | if (Meteor.isServer) { 38 | 39 | if (Schedules.find({}).fetch().length == 0){ 40 | var schedules = EJSON.parse(Assets.getText("schedules.json")); 41 | _.each(schedules, function(s){ 42 | Schedules.insert(s); 43 | }); 44 | } 45 | 46 | if (MasterSchedule.find({}).fetch().length == 0){ 47 | MasterSchedule.insert({ 48 | 'mon': 'weekday', 49 | 'tue': 'weekday', 50 | 'wed': 'weekday', 51 | 'thu': 'weekday', 52 | 'fri': 'weekday', 53 | 'sat': 'weekend', 54 | 'sun': 'weekend', 55 | }); 56 | } 57 | 58 | if (Rooms.find({}).fetch().length == 0){ 59 | var rooms = EJSON.parse(Assets.getText(Meteor.settings.roomsfile)); 60 | _.each(rooms, function(r){ 61 | Rooms.insert(r); 62 | }); 63 | } 64 | 65 | Meteor.publish("master_schedule", function () { 66 | return MasterSchedule.find({}); 67 | }); 68 | Meteor.publish("schedules", function () { 69 | return Schedules.find({}); 70 | }); 71 | Meteor.publish("hvac", function () { 72 | return HVAC.find({}); 73 | }); 74 | Meteor.publish("lighting", function () { 75 | return Lighting.find({}); 76 | }); 77 | Meteor.publish("points", function () { 78 | return Points.find({}); 79 | }); 80 | Meteor.publish("monitoring", function () { 81 | return Monitoring.find({}); 82 | }); 83 | Meteor.publish("unconfigured", function () { 84 | return Unconfigured.find({}); 85 | }); 86 | Meteor.publish("site", function () { 87 | return Site.find({}); 88 | }); 89 | 90 | } 91 | 92 | Meteor.startup(function(){ 93 | if (Meteor.isServer){ 94 | 95 | // add default admin user (default password in settings) 96 | if (!Meteor.users.find().count()){ 97 | var user = { 98 | username: 'admin', 99 | password: Meteor.settings.default_password, 100 | }; 101 | Accounts.createUser(user); 102 | } 103 | 104 | Accounts.config({ 105 | forbidClientAccountCreation: true 106 | }); 107 | 108 | Accounts.onLogin(function(){ 109 | Router.go('/'); 110 | }); 111 | 112 | Meteor.settings.public.project_root = process.env.PWD; 113 | 114 | }; 115 | 116 | FloorplansFS = new FS.Collection("floorplans_fs", { 117 | stores: [new FS.Store.FileSystem("images", {path: Meteor.settings.public.project_root + "/public/floorplans"})] 118 | }); 119 | 120 | Site.upsert({'_id':'Site'},{'_id': 'Site', 'Site': Meteor.settings.public.site}); 121 | }); 122 | -------------------------------------------------------------------------------- /openbas/lib/router.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Regents of the University of California 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, this 8 | // list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | Router.onBeforeAction(function(){ 26 | if (!(Meteor.loggingIn() || Meteor.user())) { 27 | this.render('login'); 28 | pause(); 29 | } 30 | }); 31 | 32 | Router.map(function() { 33 | this.route('dashboard', { 34 | path: '/', 35 | waitOn: function() { 36 | return [ 37 | Meteor.subscribe('schedules'), 38 | Meteor.subscribe('master_schedule'), 39 | Meteor.subscribe('points'), 40 | Meteor.subscribe('hvac'), 41 | Meteor.subscribe('lighting'), 42 | Meteor.subscribe('monitoring') 43 | ]; 44 | }, 45 | action: function () { 46 | if (this.ready()){ 47 | this.render(); 48 | } else { 49 | this.render('loading'); 50 | } 51 | } 52 | }); 53 | 54 | this.route('schedule', { 55 | waitOn: function(){ 56 | return [ 57 | Meteor.subscribe('schedules'), 58 | Meteor.subscribe('master_schedule') 59 | ]; 60 | }, 61 | }); 62 | 63 | this.route('status', { 64 | onBeforeAction: function() { 65 | Meteor.call('querysystem'); 66 | this.subscribe('hvac').wait(); 67 | this.subscribe('monitoring').wait(); 68 | this.subscribe('lighting').wait(); 69 | this.subscribe('points').wait(); 70 | this.subscribe('unconfigured').wait(); 71 | this.subscribe('site').wait(); 72 | }, 73 | }); 74 | 75 | this.route('points'); 76 | this.route('about'); 77 | this.route('plot'); 78 | 79 | this.route('zone_detail', { 80 | path: '/dashboard/:zonetype/:zone', 81 | waitOn: function() { 82 | return [ 83 | Meteor.subscribe('points'), 84 | Meteor.subscribe('hvac'), 85 | Meteor.subscribe('lighting'), 86 | Meteor.subscribe('monitoring') 87 | ]; 88 | }, 89 | data: function() { 90 | if (this.params.zonetype == 'hvac') { 91 | return {'type': 'hvac', 'points': HVAC.find({'hvaczone': this.params.zone}).fetch()}; 92 | } else if (this.params.zonetype == 'lighting') { 93 | return {'type': 'lighting', 'points': Lighting.find({'lightingzone': this.params.zone}).fetch()}; 94 | } else { 95 | return 0 96 | } 97 | } 98 | }); 99 | 100 | this.route('room_detail', { 101 | path: '/room/:room', 102 | data: function(){ 103 | return { 104 | 'room': Rooms.findOne({'RoomNumber': this.params.room}), 105 | 'general_controllers': GeneralControl.find({'room': this.params.room}) }; 106 | } 107 | }); 108 | 109 | this.route('pointDetail', { 110 | path: '/points/:uuid', 111 | data: function() { return Points.findOne({uuid: this.params.uuid}); }, 112 | }); 113 | 114 | this.route('view_schedule', { 115 | path: '/schedule/view/:id', 116 | data: function() { return Schedules.findOne({_id: this.params.id}); }, 117 | }); 118 | 119 | this.route('edit_schedule', { 120 | path: '/schedule/edit/:id', 121 | data: function() { return Schedules.findOne({_id: this.params.id}); }, 122 | }); 123 | 124 | this.route('add_schedule', { 125 | path: '/schedule/add', 126 | data: {'iperiod': 0}, 127 | }); 128 | 129 | this.route('building', { 130 | waitOn: function(){ 131 | return [Meteor.subscribe("rooms"), Meteor.subscribe("floorplans")]; 132 | }, 133 | }); 134 | 135 | this.route('add_room', { 136 | path: '/building/add_room', 137 | waitOn: function(){ 138 | return [Meteor.subscribe("rooms"), Meteor.subscribe("floorplans")]; 139 | } 140 | }); 141 | 142 | this.route('edit_room', { 143 | path: '/building/edit_room/:id', 144 | data: function() { return Rooms.findOne({_id: this.params.id}); }, 145 | waitOn: function(){ 146 | return [Meteor.subscribe("rooms"), Meteor.subscribe("floorplans")]; 147 | } 148 | }); 149 | 150 | this.route('view_room', { 151 | path: '/building/view_room/:id', 152 | data: function(){ 153 | return Rooms.findOne({_id: this.params.id}) 154 | }, 155 | }); 156 | 157 | }); 158 | 159 | Router._filters = { 160 | resetScroll: function () { 161 | var scrollTo = window.currentScroll || 0; 162 | $('body').scrollTop(scrollTo); 163 | $('body').css("min-height", 0); 164 | } 165 | }; 166 | 167 | var filters = Router._filters; 168 | 169 | if(Meteor.isClient) { 170 | Router.onAfterAction(filters.resetScroll); // for all pages 171 | } 172 | -------------------------------------------------------------------------------- /openbas/packages/.gitignore: -------------------------------------------------------------------------------- 1 | /iron-router 2 | /blaze-layout 3 | /accounts-ui-bootstrap-3 4 | /bootstrap-3 5 | /houston 6 | /paginated-subscription 7 | /spin 8 | /bootstrap-slider 9 | /iron-layout 10 | /iron-core 11 | /iron-dynamic-template 12 | /moment 13 | /jquery-ui 14 | /jquery-ui-bootstrap 15 | /jqueryui 16 | /build-fetcher 17 | /collectionFS 18 | /cfs-base-package 19 | /cfs-file 20 | /cfs-collection 21 | /cfs-collection-filters 22 | /cfs-access-point 23 | /cfs-worker 24 | /cfs-upload-http 25 | /cfs-filesaver 26 | /cfs-storage-adapter 27 | /http-methods 28 | /numeral 29 | /data-man 30 | /emitter 31 | /cfs-upload-ddp 32 | /cfs-tempstore 33 | /collectionfs 34 | /http-publish 35 | /cfs-gridfs 36 | /power-queue 37 | /reactive-list 38 | /cfs-graphicsmagick 39 | /cfs-ejson-file 40 | /cfs-filesystem 41 | /reactive-property 42 | /micro-queue 43 | -------------------------------------------------------------------------------- /openbas/packages/anytime: -------------------------------------------------------------------------------- 1 | ../../upmu-plotter/upmuplot/packages/anytime -------------------------------------------------------------------------------- /openbas/packages/col-resizable: -------------------------------------------------------------------------------- 1 | ../../upmu-plotter/upmuplot/packages/col-resizable -------------------------------------------------------------------------------- /openbas/packages/jquery-migrate: -------------------------------------------------------------------------------- 1 | ../../upmu-plotter/upmuplot/packages/jquery-migrate/ -------------------------------------------------------------------------------- /openbas/packages/jquery-simplecolorpicker: -------------------------------------------------------------------------------- 1 | ../../upmu-plotter/upmuplot/packages/jquery-simplecolorpicker -------------------------------------------------------------------------------- /openbas/packages/newd3: -------------------------------------------------------------------------------- 1 | ../../upmu-plotter/upmuplot/packages/newd3 -------------------------------------------------------------------------------- /openbas/packages/s3ui: -------------------------------------------------------------------------------- 1 | ../../upmu-plotter/upmuplot/packages/s3ui -------------------------------------------------------------------------------- /openbas/packages/timezone-js: -------------------------------------------------------------------------------- 1 | ../../upmu-plotter/upmuplot/packages/timezone-js/ -------------------------------------------------------------------------------- /openbas/packages/vakata-jstree: -------------------------------------------------------------------------------- 1 | ../../upmu-plotter/upmuplot/packages/vakata-jstree -------------------------------------------------------------------------------- /openbas/private/rooms.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "RoomNumber":205, 4 | "Description":"Office", 5 | "Exposure":"S", 6 | "HVACZone":"South", 7 | "LightingZone":"" 8 | }, 9 | { 10 | "RoomNumber":206, 11 | "Description":"Office", 12 | "Exposure":"S", 13 | "HVACZone":"South", 14 | "LightingZone":"" 15 | }, 16 | { 17 | "RoomNumber":207, 18 | "Description":"Conference", 19 | "Exposure":"S", 20 | "HVACZone":"South", 21 | "LightingZone":"" 22 | }, 23 | { 24 | "RoomNumber":208, 25 | "Description":"Kitchen", 26 | "Exposure":"S", 27 | "HVACZone":"South", 28 | "LightingZone":"" 29 | }, 30 | { 31 | "RoomNumber":209, 32 | "Description":"Office", 33 | "Exposure":"S", 34 | "HVACZone":"South", 35 | "LightingZone":"" 36 | }, 37 | { 38 | "RoomNumber":210, 39 | "Description":"Office", 40 | "Exposure":"S", 41 | "HVACZone":"South", 42 | "LightingZone":"" 43 | }, 44 | { 45 | "RoomNumber":211, 46 | "Description":"Office", 47 | "Exposure":"S", 48 | "HVACZone":"South", 49 | "LightingZone":"" 50 | }, 51 | { 52 | "RoomNumber":212, 53 | "Description":"Office", 54 | "Exposure":"S", 55 | "HVACZone":"South", 56 | "LightingZone":"" 57 | }, 58 | { 59 | "RoomNumber":213, 60 | "Description":"Office", 61 | "Exposure":"SE", 62 | "HVACZone":"East", 63 | "LightingZone":"" 64 | }, 65 | { 66 | "RoomNumber":214, 67 | "Description":"Office", 68 | "Exposure":"E", 69 | "HVACZone":"East", 70 | "LightingZone":"" 71 | }, 72 | { 73 | "RoomNumber":215, 74 | "Description":"Office", 75 | "Exposure":"E", 76 | "HVACZone":"East", 77 | "LightingZone":"" 78 | }, 79 | { 80 | "RoomNumber":216, 81 | "Description":"Office", 82 | "Exposure":"E", 83 | "HVACZone":"East", 84 | "LightingZone":"" 85 | }, 86 | { 87 | "RoomNumber":217, 88 | "Description":"Office", 89 | "Exposure":"E", 90 | "HVACZone":"East", 91 | "LightingZone":"" 92 | }, 93 | { 94 | "RoomNumber":219, 95 | "Description":"Open Plan", 96 | "Exposure":"Core", 97 | "HVACZone":"General Area", 98 | "LightingZone":"" 99 | }, 100 | { 101 | "RoomNumber":221, 102 | "Description":"Open Plan", 103 | "Exposure":"Core", 104 | "HVACZone":"General Area", 105 | "LightingZone":"" 106 | }, 107 | { 108 | "RoomNumber":222, 109 | "Description":"Office", 110 | "Exposure":"Core", 111 | "HVACZone":"General Area", 112 | "LightingZone":"" 113 | }, 114 | { 115 | "RoomNumber":223, 116 | "Description":"Office", 117 | "Exposure":"Core", 118 | "HVACZone":"General Area", 119 | "LightingZone":"" 120 | }, 121 | { 122 | "RoomNumber":225, 123 | "Description":"Office", 124 | "Exposure":"Core", 125 | "HVACZone":"General Area", 126 | "LightingZone":"" 127 | }, 128 | { 129 | "RoomNumber":203, 130 | "Description":"Hallway", 131 | "Exposure":"Core", 132 | "HVACZone":"General Area", 133 | "LightingZone":"" 134 | }, 135 | { 136 | "RoomNumber":204, 137 | "Description":"Hallway", 138 | "Exposure":"Core", 139 | "HVACZone":"General Area", 140 | "LightingZone":"" 141 | }, 142 | { 143 | "RoomNumber":205, 144 | "Description":"Hallway", 145 | "Exposure":"Core", 146 | "HVACZone":"South", 147 | "LightingZone":"" 148 | }, 149 | { 150 | "RoomNumber":218, 151 | "Description":"Open Plan", 152 | "Exposure":"N", 153 | "HVACZone":"North", 154 | "LightingZone":"" 155 | }, 156 | { 157 | "RoomNumber":220, 158 | "Description":"Open Plan", 159 | "Exposure":"N", 160 | "HVACZone":"North", 161 | "LightingZone":"" 162 | }, 163 | { 164 | "RoomNumber":224, 165 | "Description":"Office", 166 | "Exposure":"N", 167 | "HVACZone":"North", 168 | "LightingZone":"" 169 | }, 170 | { 171 | "RoomNumber":226, 172 | "Description":"Open Plan", 173 | "Exposure":"N", 174 | "HVACZone":"North", 175 | "LightingZone":"" 176 | } 177 | ] 178 | -------------------------------------------------------------------------------- /openbas/private/schedules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "weekday", 4 | "periods": [ 5 | { 6 | "name": "morning", 7 | "start": "7:30", 8 | "points": [ 9 | { 10 | "path": "temp_heat", 11 | "value": 72 12 | }, 13 | { 14 | "path": "temp_cool", 15 | "value": 83 16 | }, 17 | { 18 | "path": "on", 19 | "value": 1 20 | } 21 | ] 22 | }, 23 | { 24 | "name": "afternoon", 25 | "start": "13:30", 26 | "points": [ 27 | { 28 | "path": "temp_heat", 29 | "value": 70 30 | }, 31 | { 32 | "path": "temp_cool", 33 | "value": 80 34 | }, 35 | { 36 | "path": "on", 37 | "value": 1 38 | } 39 | ] 40 | }, 41 | { 42 | "name": "evening", 43 | "start": "18:30", 44 | "points": [ 45 | { 46 | "path": "temp_heat", 47 | "value": 50 48 | }, 49 | { 50 | "path": "temp_cool", 51 | "value": 90 52 | }, 53 | { 54 | "path": "on", 55 | "value": 0 56 | } 57 | ] 58 | } 59 | ], 60 | "color": "#EDF1FA" 61 | }, 62 | { 63 | "name": "weekend", 64 | "periods": [ 65 | { 66 | "name": "morning", 67 | "start": "09:30", 68 | "points": [ 69 | { 70 | "path": "temp_heat", 71 | "value": 65 72 | }, 73 | { 74 | "path": "temp_cool", 75 | "value": 85 76 | } 77 | ] 78 | }, 79 | { 80 | "name": "afternoon", 81 | "start": "17:30", 82 | "points": [ 83 | { 84 | "path": "temp_heat", 85 | "value": 70 86 | }, 87 | { 88 | "path": "temp_cool", 89 | "value": 80 90 | } 91 | ] 92 | }, 93 | { 94 | "name": "evening", 95 | "start": "21:00", 96 | "points": [ 97 | { 98 | "path": "temp_heat", 99 | "value": 50 100 | }, 101 | { 102 | "path": "temp_cool", 103 | "value": 90 104 | } 105 | ] 106 | } 107 | ], 108 | "color": "#FAF6ED" 109 | } 110 | ] 111 | -------------------------------------------------------------------------------- /openbas/private/testrooms.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "RoomNumber":"410 Soda", 4 | "Description":"Office", 5 | "Exposure":"Core", 6 | "HVACZone":"410 W Soda", 7 | "LightingZone":"410 Soda" 8 | }, 9 | { 10 | "RoomNumber":"410 Soda", 11 | "Description":"Office", 12 | "Exposure":"Core", 13 | "HVACZone":"410 E Soda", 14 | "LightingZone":"410 Soda" 15 | }, 16 | { 17 | "RoomNumber":"411 Soda", 18 | "Description":"Office", 19 | "Exposure":"Core", 20 | "HVACZone":"411 Soda", 21 | "LightingZone":"411 Soda" 22 | }, 23 | { 24 | "RoomNumber":"420 Soda", 25 | "Description":"Office", 26 | "Exposure":"Core", 27 | "HVACZone":"420 Soda", 28 | "LightingZone":"420 Soda" 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /openbas/public/floorplans/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/openbas/public/floorplans/.DS_Store -------------------------------------------------------------------------------- /openbas/public/img/CIEE-floorplan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/openbas/public/img/CIEE-floorplan.png -------------------------------------------------------------------------------- /openbas/public/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/openbas/public/img/ajax-loader.gif -------------------------------------------------------------------------------- /openbas/public/img/debut-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareDefinedBuildings/openbas/e18dcb2e2111afa6a020cac69f7028d4a1b7a520/openbas/public/img/debut-light.png -------------------------------------------------------------------------------- /openbas/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "archiverUrl": "http://localhost:8079", 3 | "roomsfile": "testrooms.json", 4 | "default_password": "password", 5 | "apikey": "lVzBMDpnkXApJmpjUDSvm4ceGfpbrLLSd9cq", 6 | "public": { 7 | "archiverUrl": "http://localhost:8079", 8 | "site": "0273d18f-1c03-11e4-a490-6003089ed1d0", 9 | "building_name": "CIEE" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /smap-control-example/control.ini: -------------------------------------------------------------------------------- 1 | [report 0] 2 | ReportDeliveryLocation = http://localhost:8079/add/lVzBMDpnkXApJmpjUDSvm4ceGfpbrLLSd9cq 3 | 4 | [/] 5 | uuid = c4ca3cf0-34c8-11e1-9bca-030e8139310d 6 | Metadata/SourceName = Demo Driver Repub 7 | 8 | [server] 9 | port = 1235 10 | 11 | [/control] 12 | type=control.Controller 13 | archiver_url=http://localhost:8079 14 | roomuuid=e2153d3d-78c6-5712-a484-fcfb4937785a 15 | setpoint=72 16 | deadband=4 17 | rate=.1 18 | -------------------------------------------------------------------------------- /smap-control-example/control.py: -------------------------------------------------------------------------------- 1 | from smap import actuate, driver 2 | from smap.archiver.client import RepublishClient 3 | from smap.util import periodicSequentialCall 4 | import random 5 | import math 6 | 7 | from controllogic import cool_controller 8 | 9 | class Controller(driver.SmapDriver): 10 | def setup(self, opts): 11 | # source of streaming data 12 | self.archiver_url = opts.pop('archiver_url','http://localhost:8079') 13 | # streaming data uuid 14 | self.roomuuid = opts.get('roomuuid') 15 | # subscribe to datastream 16 | restriction = "uuid = '{0}'".format(self.roomuuid) 17 | self.roomclient = RepublishClient(self.archiver_url, self.controlcb, restrict=restriction) 18 | # setpoint 19 | self.sp = float(opts.get('setpoint',72)) 20 | # deadband 21 | self.db = float(opts.get('deadband',1)) 22 | # period between calling controller 23 | self.rate = float(opts.get('rate',1)) 24 | self.cool = 0 # off 25 | 26 | self.add_timeseries('/cool', 'On/Off',data_type='long') 27 | 28 | # initialize current temperature to setpoint 29 | self.cur_temp = self.sp 30 | 31 | self.cool_controller = cool_controller 32 | 33 | def start(self): 34 | self.roomclient.connect() 35 | periodicSequentialCall(self.read).start(self.rate) 36 | 37 | def read(self): 38 | # calculate and send control decision 39 | #print self.cool_controller(self.cur_temp, self.sp, self.db) 40 | #self.add('/cool', self.cool_controller(self.cur_temp, self.sp, self.db)) 41 | if self.cool: # 42 | self.cool = self.cur_temp > self.sp - self.db 43 | else: 44 | self.cool = self.cur_temp > self.sp + self.db 45 | self.add('/cool',int(self.cool)) 46 | 47 | def controlcb(self, _, data): 48 | # parse streaming data 49 | mostrecent = data[-1][-1] 50 | self.cur_temp = mostrecent[1] 51 | -------------------------------------------------------------------------------- /smap-control-example/controllogic.py: -------------------------------------------------------------------------------- 1 | # Basic controller. Decides whether to cool or not. No damping 2 | def cool_controller(current_temperature, setpoint, deadband): 3 | print current_temperature, setpoint, deadband 4 | if current_temperature > setpoint + deadband: 5 | print 'COOL' 6 | return 1 7 | else: 8 | print 'NO COOL' 9 | return 0 10 | 11 | # simple controller 12 | # def cool_controller(current_temperature, setpoint, deadband): 13 | # return int(current_temperature > setpoint + deadband) 14 | -------------------------------------------------------------------------------- /smap-control-example/oat.ini: -------------------------------------------------------------------------------- 1 | [report 0] 2 | ReportDeliveryLocation = http://localhost:8079/add/lVzBMDpnkXApJmpjUDSvm4ceGfpbrLLSd9cq 3 | 4 | [/] 5 | uuid = a4ca3cf0-34c8-11e1-9bca-030e8139310d 6 | Metadata/SourceName = Demo Driver Repub 7 | 8 | [server] 9 | port = 1234 10 | 11 | [/oat] 12 | type=oat.OAT 13 | start=80 14 | rate=.1 15 | jump=2 16 | Metadata/Extra/Type = Outside Air Temperature 17 | -------------------------------------------------------------------------------- /smap-control-example/oat.py: -------------------------------------------------------------------------------- 1 | from smap import driver 2 | from smap.archiver.client import RepublishClient 3 | from smap.util import periodicSequentialCall 4 | import math 5 | 6 | class OAT(driver.SmapDriver): 7 | def setup(self, opts): 8 | # initial temperature 9 | self.start_temp = float(opts.pop('start_temp','80')) 10 | # period between polling sensor 11 | self.rate = float(opts.pop('rate','1')) 12 | self.jump = float(opts.pop('jump','1')) 13 | self.curtemp = self.start_temp 14 | 15 | self.t = 1 16 | 17 | self.add_timeseries('/temperature','F',data_type='double') 18 | 19 | def start(self): 20 | periodicSequentialCall(self.read).start(self.rate) 21 | 22 | def read(self): 23 | # temperature calculated as a parametric sine wave 24 | self.t += 1 25 | self.curtemp = 78 + 10*math.sin(.01 * self.t) 26 | self.add('/temperature', self.curtemp) 27 | 28 | -------------------------------------------------------------------------------- /smap-control-example/room.ini: -------------------------------------------------------------------------------- 1 | [report 0] 2 | ReportDeliveryLocation = http://localhost:8079/add/lVzBMDpnkXApJmpjUDSvm4ceGfpbrLLSd9cq 3 | 4 | [/] 5 | uuid = b4ca3cf0-34c8-11e1-9bca-030e8139310d 6 | Metadata/SourceName = Demo Driver Repub 7 | 8 | [server] 9 | port = 1236 10 | 11 | [/room] 12 | type=room.Room 13 | oatuuid=9a846841-0834-5045-a0ae-4a9c57cebb0c 14 | cooluuid=c02b8f41-8da9-5c04-9020-58a135ba5879 15 | rate=.1 16 | therm_resistance=.1 17 | epsilon=.15 18 | starttemp=70 19 | archiver_url=http://localhost:8079 20 | 21 | -------------------------------------------------------------------------------- /smap-control-example/room.py: -------------------------------------------------------------------------------- 1 | from smap import actuate, driver 2 | from smap.archiver.client import RepublishClient 3 | from smap.util import periodicSequentialCall 4 | import random 5 | import math 6 | 7 | class Room(driver.SmapDriver): 8 | def setup(self, opts): 9 | # source of streaming data 10 | self.archiver_url = opts.pop('archiver_url','http://localhost:8079') 11 | # streaming data uuid 12 | self.oatuuid = opts.get('oatuuid') 13 | # subscribe to datastream 14 | restriction = "uuid = '{0}'".format(self.oatuuid) 15 | self.oatclient = RepublishClient(self.archiver_url, self.oatcb, restrict=restriction) 16 | 17 | self.cooluuid = opts.get('cooluuid') 18 | restriction = "uuid = '{0}'".format(self.cooluuid) 19 | self.coolclient = RepublishClient(self.archiver_url, self.coolcb, restrict=restriction) 20 | 21 | # rate between recalculating room model 22 | self.rate = float(opts.get('rate',1)) 23 | # thermal resistance of room 24 | self.therm_resistance = float(opts.get('therm_resistance', .1)) 25 | # initial temperature of room 26 | self.temp = float(opts.get('starttemp', 75)) 27 | # epsilon for room (for cooling) 28 | self.e = float(opts.get('epsilon',.1)) 29 | 30 | # state vars 31 | self.oat_val = 0 32 | self.cool = 0 33 | 34 | self.add_timeseries('/currenttemp','F',data_type='double') 35 | 36 | def start(self): 37 | self.oatclient.connect() 38 | self.coolclient.connect() 39 | periodicSequentialCall(self.read).start(self.rate) 40 | 41 | def read(self): 42 | dt = (self.oat_val - self.temp) 43 | print self.temp, self.rate, self.therm_resistance, self.cool,self.e 44 | self.temp = self.temp + self.rate * self.therm_resistance * dt - self.cool*self.e*dt 45 | self.add('/currenttemp', self.temp) 46 | 47 | def oatcb(self, _, data): 48 | # list of arrays of [time, val] 49 | mostrecent = data[-1][-1] 50 | self.oat_val = mostrecent[1] 51 | 52 | def coolcb(self, _, data): 53 | # list of arrays of [time, val] 54 | mostrecent = data[-1][-1] 55 | self.cool = mostrecent[1] 56 | print 'am i cooling', self.cool 57 | -------------------------------------------------------------------------------- /smap-control-example/startdrivers.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | 4 | inis = sys.argv[1:] 5 | 6 | processes = [] 7 | for ini in inis: 8 | command = "twistd --pidfile {0}.pid -n smap {0}.ini".format(ini) 9 | p = subprocess.Popen(command, shell=True) 10 | processes.append(p) 11 | 12 | try: 13 | raw_input() 14 | except: 15 | for p in processes: 16 | print "killing",p 17 | p.terminate() 18 | -------------------------------------------------------------------------------- /smap-control-tutorial/control-demo.ini: -------------------------------------------------------------------------------- 1 | # Set to api key for the particular site 2 | [report 0] 3 | ReportDeliveryLocation = http://localhost:8079/add/8KwKzvbVjZZ7AlQzEwBjR7LUPRiwWnh2jg3D 4 | 5 | # unique resource for this demonstration 6 | [/] 7 | # unique identifier for the collection 8 | uuid = c14af954-1686-11e4-9685-000c29b778da 9 | 10 | Metadata/SourceName = Pseudo Room Control Demonstration 11 | 12 | Metadata/Site/Name = Imaginary Building 13 | # unique identifier for the site, which may have many collections 14 | Metadata/Site/id = 2c8ed966-146a-11e4-9e46-000c29b778da 15 | 16 | # REST resource for control demo 17 | [server] 18 | port = 1236 19 | 20 | [/control] 21 | type=coolControllerService.Controller 22 | setpoint=72 23 | deadband=2 24 | rate=1 25 | archiver_url=http://localhost:8079 26 | zonepath = /room/airtemp 27 | zonesiteid = 2c8ed966-146a-11e4-9e46-000c29b778da 28 | 29 | [/control/cool] 30 | Metadata/Description = Cooling request 31 | Metadata/Type = Command 32 | -------------------------------------------------------------------------------- /smap-control-tutorial/coolControllerService.py: -------------------------------------------------------------------------------- 1 | from smap import actuate, driver 2 | from smap.archiver.client import RepublishClient 3 | from smap.util import periodicSequentialCall 4 | import math 5 | 6 | class Controller(driver.SmapDriver): 7 | def setup(self, opts): 8 | self.archiver_url = opts.pop('archiver_url','http://localhost:8079') 9 | self.sp = float(opts.get('setpoint',78)) 10 | self.db = float(opts.get('deadband',1)) 11 | self.rate = float(opts.get('rate',1)) 12 | 13 | # Initialize controller state 14 | self.cur_temp = self.sp 15 | self.state = 0 # initial cool is off 16 | 17 | # subscribe to zone air temperature 18 | self.zonepath = opts.pop('zonepath','/room/airtemp') 19 | self.siteid = opts.pop('zonesiteid','') 20 | restriction = "Path = '{0}'".format(self.zonepath) 21 | if self.siteid : 22 | restriction = restriction + " and Metadata/Site/id = '{0}'".format(self.siteid) 23 | self.roomclient = RepublishClient(self.archiver_url, self.controlcb, restrict=restriction) 24 | 25 | # create timeseries for contoller actions 26 | self.add_timeseries('/cool', 'On/Off', data_type='long') 27 | 28 | def start(self): 29 | self.roomclient.connect() 30 | periodicSequentialCall(self.read).start(self.rate) 31 | 32 | # Periodically schedule controller event 33 | # - update control state 34 | # - build timeseries of actions 35 | def read(self): 36 | if (self.cur_temp > self.sp + self.db) : self.state = 1 # start cool 37 | if (self.cur_temp < self.sp - self.db) : self.state = 0 # stop cool 38 | self.add('/cool', self.state) # publish the state change 39 | 40 | # Handle temperature reporting event 41 | # record most recent zone temperature for next contol event 42 | def controlcb(self, _, data): 43 | mostrecent = data[-1][-1] 44 | self.cur_temp = mostrecent[1] 45 | -------------------------------------------------------------------------------- /smap-control-tutorial/oat.ini: -------------------------------------------------------------------------------- 1 | # Configuration for a collection monitoring the outside environment 2 | # The apikey must be pre-established on archiver host by adding a suscription 3 | [report 0] 4 | ReportDeliveryLocation = http://localhost:8079/add/8KwKzvbVjZZ7AlQzEwBjR7LUPRiwWnh2jg3D 5 | 6 | [/] 7 | uuid = a4ca3cf0-34c8-11e1-9bca-030e8139310d 8 | Metadata/SourceName = Demonstration outside air temperature 9 | 10 | Metadata/Site/Name = Imaginary Building 11 | # unique identifier for the site, which may have many collections 12 | Metadata/Site/id = 2c8ed966-146a-11e4-9e46-000c29b778da 13 | 14 | # REST resource for this source is http://driverhost:port 15 | [server] 16 | port = 1234 17 | 18 | # outside environment collection 19 | # The name of the resource matches the class provided by the driver 20 | 21 | # Model of outside world provided by a time varying sequence of values by pseudo-sensor 22 | # parameters provided to the driver describe its behavior 23 | # this section would be replaced by a real driver 24 | [/OAT] 25 | type=pseudoOATdriver.OAT 26 | start=78 27 | rate=1 28 | jump=2 29 | Metadata/Description = Pseudo sensor generating simulated OAT 30 | 31 | [/OAT/temperature] 32 | Metadata/Type = Sensor 33 | Metadata/Kind = Temperature 34 | Metadata/Form = Air 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /smap-control-tutorial/oat.py: -------------------------------------------------------------------------------- 1 | from smap import driver 2 | from smap.archiver.client import RepublishClient 3 | from smap.util import periodicSequentialCall 4 | import math 5 | 6 | class OAT(driver.SmapDriver): 7 | def setup(self, opts): 8 | self.starttemp = float(opts.pop('start_temp','80')) 9 | self.rate = float(opts.pop('rate','1')) 10 | self.jump = float(opts.pop('jump','1')) 11 | self.curtemp = self.starttemp 12 | 13 | self.t = 1 14 | 15 | self.add_timeseries('/temperature','F',data_type='double') 16 | 17 | def start(self): 18 | periodicSequentialCall(self.read).start(self.rate) 19 | 20 | def read(self): 21 | self.t += 1 22 | self.curtemp = 78 + 10*math.sin(.01 * self.t) 23 | self.add('/temperature', self.curtemp) 24 | 25 | -------------------------------------------------------------------------------- /smap-control-tutorial/pseudoOATdriver.py: -------------------------------------------------------------------------------- 1 | # Pseudo outside air temperature driver 2 | # Produces a stream of values varying around a given point 3 | # 4 | # Configuration parameters example 5 | # 6 | # [/oat] 7 | # type=pseudoOATdriver.OAT 8 | # start=80 - initial temperature 9 | # rate=1 - period of report 10 | # jump=2 - width of variation 11 | # 12 | from smap import driver 13 | from smap.util import periodicSequentialCall 14 | import math 15 | 16 | class OAT(driver.SmapDriver): 17 | def setup(self, opts): 18 | self.starttemp = float(opts.pop('start_temp','80')) 19 | self.rate = float(opts.pop('rate','1')) 20 | self.jump = float(opts.pop('jump','2')) 21 | self.curtemp = self.starttemp 22 | self.t = 1 23 | self.add_timeseries('/temperature','F',data_type='double') 24 | 25 | def start(self): 26 | periodicSequentialCall(self.read).start(self.rate) 27 | 28 | def read(self): 29 | self.t += 1 30 | self.curtemp = self.starttemp + self.jump*math.sin(.01 * self.t) 31 | self.add('/temperature', self.curtemp) 32 | 33 | -------------------------------------------------------------------------------- /smap-control-tutorial/room.ini: -------------------------------------------------------------------------------- 1 | # Configuration for a the room model, represented by a pseudo air temp 2 | # Metadata links it to common building 3 | # Operates as an independent source on its own thread 4 | 5 | # Archiver with API key 6 | [report 0] 7 | ReportDeliveryLocation = http://localhost:8079/add/8KwKzvbVjZZ7AlQzEwBjR7LUPRiwWnh2jg3D 8 | 9 | [/] 10 | uuid = 1814f354-1432-11e4-9cec-000c29b778da 11 | Metadata/SourceName = Demontration Room Model with Air Conditioner cooling 12 | 13 | Metadata/Site/Name = Imaginary Building 14 | Metadata/Site/id = 2c8ed966-146a-11e4-9e46-000c29b778da 15 | 16 | [server] 17 | port = 1237 18 | 19 | [/room] 20 | type=roommodeldriver.Room 21 | rate=1 22 | # warm at 100 s per degree diff, cool at 20s per degree 23 | therm_resistance=.01 24 | epsilon=.05 25 | starttemp=72 26 | archiver_url=http://localhost:8079 27 | OATpath = /OAT/temperature 28 | coolpath = /control/cool 29 | siteid = 2c8ed966-146a-11e4-9e46-000c29b778da 30 | 31 | [/room/airtemp] 32 | Metadata/Description = Pseudo sensor modeling room air temperature 33 | Metadata/Type = Sensor 34 | Metadata/Kind = Temperature 35 | Metadata/Form = Air 36 | 37 | -------------------------------------------------------------------------------- /smap-control-tutorial/roommodeldriver.py: -------------------------------------------------------------------------------- 1 | from smap import actuate, driver 2 | from smap.archiver.client import RepublishClient 3 | from smap.util import periodicSequentialCall 4 | import random 5 | import math 6 | 7 | class Room(driver.SmapDriver): 8 | def badclient(self, resp): 9 | print "Error connecting: ", resp 10 | 11 | def setup(self, opts): 12 | self.archiver_url = opts.pop('archiver_url','http://localhost:8079') 13 | self.oatpath = opts.pop('OATpath','/OAT/temperature') 14 | self.coolpath = opts.pop('coolpath', '/control/cool') 15 | self.siteid = opts.pop('siteid','') 16 | 17 | # Subscription to OAT stream, registering a callback 18 | restriction = "Path = '{0}'".format(self.oatpath) 19 | if self.siteid : 20 | restriction = restriction + " and Metadata/Site/id = '{0}'".format(self.siteid) 21 | self.oatclient = RepublishClient(self.archiver_url, self.oatcb, restrict=restriction, connect_error=self.badclient) 22 | 23 | # subscribe to the cool control stream 24 | restriction = "Path = '{0}'".format(self.coolpath) 25 | if self.siteid : 26 | restriction = restriction + " and Metadata/Site/id = '{0}'".format(self.siteid) 27 | 28 | self.coolclient = RepublishClient(self.archiver_url, self.coolcb, restrict=restriction, connect_error=self.badclient) 29 | 30 | # initalize parameters for the room model 31 | self.rate = float(opts.get('rate',1)) # model update rate 32 | self.therm_resistance = float(opts.get('therm_resistance', .1)) # thermal resistance factor 33 | self.e = float(opts.get('epsilon',.1)) # cooling factor 34 | 35 | # initial state of the room 36 | self.temp = float(opts.get('starttemp', 75)) # initial room 37 | self.oat_val = self.temp 38 | self.cool = 0 39 | 40 | # Create the timeseries for the pseudo air temp sensor 41 | self.add_timeseries('/airtemp','F',data_type='double') 42 | 43 | # start the driver 44 | def start(self): 45 | self.oatclient.connect() # activate subscription to OAT stream' 46 | self.coolclient.connect() # activate subscription to cool stream 47 | periodicSequentialCall(self.read).start(self.rate) # schedule model periodically 48 | 49 | # Model simple physics of a room with heating across a thermally 50 | # resistant barrier and forced cooling 51 | def read(self): 52 | dt = (self.oat_val - self.temp) 53 | # self.temp = self.oat_val + 2.3 # little test during debugging 54 | self.temp = self.temp + self.rate * self.therm_resistance * dt - self.cool*self.e*dt 55 | self.add('/airtemp', self.temp) # add new value to the airtemp stream 56 | 57 | # Event handler for publication to OAT stream 58 | def oatcb(self, _, data): 59 | # list of arrays of [time, val] 60 | mostrecent = data[-1][-1] 61 | self.oat_val = mostrecent[1] 62 | 63 | # Event handler for publication to cool stream 64 | def coolcb(self, _, data): 65 | mostrecent = data[-1][-1] 66 | self.cool = mostrecent[1] 67 | -------------------------------------------------------------------------------- /smap-control-tutorial/schedule-demo.ini: -------------------------------------------------------------------------------- 1 | # Set to api key for the particular site 2 | [report 0] 3 | ReportDeliveryLocation = http://localhost:8079/add/8KwKzvbVjZZ7AlQzEwBjR7LUPRiwWnh2jg3D 4 | 5 | # unique resource for this demonstration 6 | [/] 7 | # unique identifier for the collection 8 | uuid = 8b37a536-1ebc-11e4-9e51-000c29b778da 9 | 10 | Metadata/SourceName = Schedule Demonstration 11 | 12 | Metadata/Site/Name = Imaginary Building 13 | # unique identifier for the site, which may have many collections 14 | Metadata/Site/id = 2c8ed966-146a-11e4-9e46-000c29b778da 15 | 16 | # REST resource for control demo 17 | [server] 18 | port = 1240 19 | 20 | [/scheduler] 21 | type = schedulerService.Scheduler 22 | rate = 15 23 | defaultHeatSetpoint = 68 24 | defaultCoolSetpoint = 76 25 | 26 | [/scheduler/heatSetpoint] 27 | Metadata/Description = Heating setpoint 28 | Metadata/Type = Setpoint 29 | 30 | [/scheduler/coolSetpoint] 31 | Metadata/Description = Cooling setpoint 32 | Metadata/Type = Setpoint 33 | 34 | [/thermostat1] 35 | type=virtualthermostatDriver.VirtualThermostat 36 | Metadata/Description = Virtual Thermostat for Zone 1 37 | archiver_url=http://localhost:8079 38 | heatSPpath = /scheduler/heatSetpoint 39 | coolSPpath = /scheduler/coolSetpoint 40 | trim = 1 41 | 42 | [/thermostat2] 43 | type=virtualthermostatDriver.VirtualThermostat 44 | Metadata/Description = Virtual Thermostat for Zone 2 45 | archiver_url=http://localhost:8079 46 | heatSPpath = /scheduler/heatSetpoint 47 | coolSPpath = /scheduler/coolSetpoint 48 | trim = -1 49 | -------------------------------------------------------------------------------- /smap-control-tutorial/schedule.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 20142, Regents of the University of California 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | - Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | - Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the 14 | distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 19 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 20 | THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 25 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 27 | OF THE POSSIBILITY OF SUCH DAMAGE. 28 | """ 29 | """ 30 | @author David Culler 31 | """ 32 | 33 | from copy import copy 34 | 35 | def cvt(hr,mn) :return hr+mn/60.0 36 | 37 | class scheduler(): 38 | """ 39 | Scheduler class to hold and manage a set of named schedules, 40 | each constisting of an ordered sequence of named epochs 41 | with a start time, as ':', and a dictionary of :value 42 | 43 | """ 44 | schedules = {} 45 | 46 | def __init__(self, initial={}) : 47 | """ 48 | Create a scheduler, optionally with a dict of schedules 49 | {:((,,{})*)*} 50 | 51 | traverses initial schedules to validate and transfer to internal representation 52 | 53 | """ 54 | for schedName, sched in initial.iteritems() : 55 | self.addSchedule(schedName, sched) 56 | 57 | def getSchedules(self) : 58 | """ 59 | Return the sequence of schedule names in the scheduler 60 | """ 61 | return [name for name, schedule in self.schedules.iteritems()] 62 | 63 | def getSchedule(self, scheduleName) : 64 | """ 65 | Return the list of (,,) epochs for the named schedule 66 | """ 67 | return copy(self.schedules[scheduleName]) if scheduleName in self.schedules else {} 68 | 69 | def addSchedule(self, scheduleName, schedule) : 70 | """ 71 | Add a schedule to the scheduler, replacing any schedule with the same name 72 | """ 73 | self.schedules[scheduleName] = [(name, start, points) for name, start, points in schedule] 74 | 75 | def delSchedule(self,scheduleName) : 76 | """ 77 | Delete a schedule for the scheduler 78 | """ 79 | del self.schedules[scheduleName] 80 | 81 | def getPoints(self, scheduleName, hour, minute) : 82 | """ 83 | Return the epoch name, points for the epoch covered by the specified time 84 | """ 85 | # Lookup named schedule in schedules database 86 | # Return: (epoch, points) 87 | # epoch: name of the epoch containing hour:minute 88 | # points: dictionary of points for the prior enclosing epoch 89 | # 'none', {} if schedule or enclosing epoch does not exist 90 | epoch, prev = 'none', {} 91 | target = cvt(hour, minute) 92 | if (scheduleName in self.schedules) : 93 | schedule = self.schedules[scheduleName] 94 | for ep,start,points in schedule : 95 | hr,mn = start.split(':') 96 | nextTime = cvt(int(hr),int(mn)) 97 | if (target < nextTime) : break 98 | epoch, prev = ep, points 99 | return epoch, copy(prev) 100 | 101 | def getFinalPoints(self, scheduleName) : 102 | """ 103 | Return the epoch name, points for the final epoch in a schedule 104 | 105 | This is useful for constructing cyclic schedules 106 | """ 107 | if (scheduleName in self.schedules) : 108 | schedule = self.schedules[scheduleName] 109 | name, start, points = schedule[-1] 110 | return name, copy(points) 111 | else : 112 | return 'none',{} 113 | 114 | def generateDay(self, scheduleName, minutes=30, initial='') : 115 | """ 116 | Generate and iterator that will produce a periodic series 117 | of eopch,points for an day under a schedule 118 | 119 | initial, if provided, starts the series with the final epoch of the 120 | specified schedule 121 | """ 122 | name, state = 'none', {} 123 | if initial : name, state = self.getFinalPoints(initial) 124 | for m in range(0,24*60,minutes) : 125 | hour = m/60 126 | min = m - hour*60 127 | ename, points = self.getPoints(scheduleName,hour,min) 128 | if ename != 'none' : name = ename 129 | for point, val in points.iteritems() : state[point] = val 130 | yield (hour, min, name, state) 131 | 132 | if __name__ == '__main__': 133 | testschedules = {'weekday': ( 134 | ('morning', '7:30', {'setpoint':72, 'light':'on'}), 135 | ('evening','17:00', {'setpoint':75, 'light':'on'}), 136 | ('night', '20:30', {'setpoint':54, 'light':'off'}) 137 | ), 138 | 'weekend': ( 139 | ('morning', '9:30', {'setpoint':71, 'light':'on'}), 140 | ('evening','17:00', {'setpoint':75, 'light':'on'}), 141 | ('night', '21:00', {'setpoint':57, 'light':'off'}) 142 | ) 143 | } 144 | s = scheduler(testschedules) 145 | for x in s.generateDay('weekday', 30, 'weekend') : print x 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /smap-control-tutorial/schedulerService.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2014, Regents of the University of California 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | - Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | - Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the 14 | distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 19 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 20 | THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 25 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 27 | OF THE POSSIBILITY OF SUCH DAMAGE. 28 | """ 29 | """ 30 | @author David Culler 31 | """ 32 | from smap import actuate, driver 33 | from smap.util import periodicSequentialCall 34 | import schedule 35 | import time 36 | 37 | testschedules = {'weekday': (('morning', '8:00', {'heatSetpoint':68, 'coolSetpoint': 75}), 38 | ('night', '18:00', {'heatSetpoint':55, 'coolSetpoint': 85})), 39 | 'weekend': (('morning', '9:30', {'heatSetpoint':68, 'coolSetpoint': 75}), 40 | ('evening','14:00', {'heatSetpoint':55, 'coolSetpoint': 85}), 41 | ('night', '21:00', {'heatSetpoint':55, 'coolSetpoint': 85}))} 42 | 43 | class Scheduler(driver.SmapDriver): 44 | def setup(self, opts): 45 | self.rate = float(opts.get('rate',15)) 46 | self.sched = schedule.scheduler(testschedules) 47 | 48 | # initialize weekly schedule - uses date.weekday convention 49 | self.weekly = ('weekday','weekday','weekday','weekday','weekday','weekend','weekend') 50 | self.schedule = 'none' 51 | self.epoch = 'none' 52 | 53 | # Current state of the points 54 | self.points = {} 55 | self.points['heatSetpoint']=int(opts.get('defaultHeatSetpoint',68)) 56 | self.points['coolSetpoint']=int(opts.get('defaultCoolSetpoint',76)) 57 | print "Scheduler ", self.points 58 | 59 | # create timeseries for scheduler actions 60 | heatSetPoint = self.add_timeseries('/heatSetpoint', 'F', data_type='long') 61 | coolSetPoint = self.add_timeseries('/coolSetpoint', 'F', data_type='long') 62 | 63 | # ought to have schedule and epoch be timeseries too. This forces enumerate 64 | heatSetPoint.add_actuator(setpointActuator(scheduler=self, range=(40,90))) 65 | coolSetPoint.add_actuator(setpointActuator(scheduler=self, range=(40,90))) 66 | 67 | def start(self): 68 | print "scheduler start: ", self.rate 69 | periodicSequentialCall(self.read).start(self.rate) 70 | 71 | # Periodically check the schedule 72 | # when there is a change publish to thermostats, will persist till new epoch or overide 73 | def read(self): 74 | now = time.localtime() 75 | scheduleName = self.weekly[now.tm_wday] 76 | self.epoch, self.points = self.sched.getPoints(scheduleName, now.tm_hour, now.tm_min) 77 | print "scheduler read: ", self.epoch, self.points 78 | for (point,val) in self.points.iteritems() : 79 | self.points[point] = val 80 | self.add('/'+point, val) 81 | print "scheduler points: ", self.points 82 | 83 | class setpointActuator(actuate.ContinuousActuator): 84 | def __init__(self, **opts): 85 | actuate.ContinuousActuator.__init__(self, range=(40,100)) 86 | self.scheduler = opts.get('scheduler') 87 | 88 | def get_state(self, request): 89 | return self.scheduler.points 90 | 91 | def set_state(self, request, val): 92 | print "set state", request, val 93 | # self.scheduler.sp = val 94 | return state 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /smap-control-tutorial/schedulerZoneDemo.ini: -------------------------------------------------------------------------------- 1 | # Set to api key for the particular application 2 | [report 0] 3 | ReportDeliveryLocation = http://localhost:8079/add/8KwKzvbVjZZ7AlQzEwBjR7LUPRiwWnh2jg3D 4 | 5 | # unique resource for this demonstration 6 | [/] 7 | 8 | # unique identifier for the collection 9 | uuid = 8b37a536-1ebc-11e4-9e51-000c29b778da 10 | 11 | Metadata/SourceName = Schedule Demonstration 12 | Metadata/Site/Name = Imaginary Building 13 | # unique identifier for the site, which may have many collections 14 | Metadata/Site/id = 2c8ed966-146a-11e4-9e46-000c29b778da 15 | 16 | # REST resource for control demo 17 | [server] 18 | port = 1240 19 | 20 | # Scheduler operating across hvac and lighting 21 | [/scheduler] 22 | type = schedulerService.Scheduler 23 | rate = 10 24 | defaultHeatSetpoint = 70 25 | defaultCoolSetpoint = 74 26 | archiver_url=http://localhost:8079 27 | 28 | [/scheduler/heatSetpoint] 29 | Metadata/Description = Master Heating setpoint 30 | Metadata/Type = Setpoint 31 | 32 | [/scheduler/coolSetpoint] 33 | Metadata/Description = Master Cooling setpoint 34 | Metadata/Type = Setpoint 35 | 36 | [/buildinghvac] 37 | type = Collection 38 | Metadata/System = HVAC 39 | Metadata/Role = Building HVAC 40 | archiver_url=http://localhost:8079 41 | 42 | # Zone with zone controller intermediary between master schedule and thermostat 43 | [/buildinghvac/zonecontroller1] 44 | type = zoneControllerService.ZoneController 45 | Metadata/Description = Zone controller for Zone 1 46 | Metadata/HVACZone = Zone1 47 | rate = 10 48 | heatSPwhere = Path="/scheduler/heatSetpoint" and Metadata/Site/id = "2c8ed966-146a-11e4-9e46-000c29b778da" 49 | coolSPwhere = Path="/scheduler/coolSetpoint" and Metadata/Site/id = "2c8ed966-146a-11e4-9e46-000c29b778da" 50 | thermwhere = Path = "/buildinghvac/thermostat1/temp" and Metadata/Site/id = "2c8ed966-146a-11e4-9e46-000c29b778da" 51 | tempwhere = Metadata/HVACZone = "Zone1" and Metadata/Site/id = "2c8ed966-146a-11e4-9e46-000c29b778da" and Metadata/Type = "Sensor" and Metadata/Kind = "Temperature" and Metadata/Form = "Air" 52 | 53 | 54 | defaultHeatSetpoint = 68 55 | defaultCoolSetpoint = 76 56 | trim = 1 57 | 58 | [/buildinghvac/thermostat1] 59 | type=virtualthermostatDriver.VirtualThermostat 60 | Metadata/Description = Virtual Thermostat for Zone 1 61 | Metadata/HVACZone = Zone1 62 | Metadata/Room = Big Room 63 | heatSPpath = /zoneController1/heatSetpoint 64 | coolSPpath = /zoneController1/coolSetpoint 65 | 66 | # zone controlled directly from master schedule 67 | [/buildinghvac/thermostat2] 68 | type=virtualthermostatDriver.VirtualThermostat 69 | Metadata/Description = Virtual Thermostat for Zone 2 70 | Metadata/HVACZone = Zone2 71 | Metadata/Room = Library 72 | archiver_url=http://localhost:8079 73 | heatSPpath = /scheduler/heatSetpoint 74 | coolSPpath = /scheduler/coolSetpoint 75 | 76 | 77 | [/monitoring] 78 | type = Collection 79 | Metadata/System = Monitoring 80 | Metadata/Role = Monitoring 81 | 82 | [/monitoring/temp1] 83 | type=temperatureDriver.Temperature 84 | Metadata/Description = Virtual Temperature Sensor for Room 2 Zone 1 85 | Metadata/Room = Little room 86 | Metadata/HVACZone = Zone1 87 | 88 | [/monitoring/temp1/temperature] 89 | Metadata/Type = Sensor 90 | Metadata/Kind = Temperature 91 | Metadata/Form = Air 92 | start=70 93 | rate=7 94 | jump=2 95 | 96 | [/monitoring/temp2] 97 | type=temperatureDriver.Temperature 98 | Metadata/Description = Virtual Temperature Sensor for Room 3 Zone 1 99 | Metadata/Room = Green room 100 | Metadata/HVACZone = Zone1 101 | 102 | [/monitoring/temp2/temperature] 103 | Metadata/Type = Sensor 104 | Metadata/Kind = Temperature 105 | Metadata/Form = Air 106 | start=68 107 | rate=5 108 | jump=2 109 | 110 | [/monitoring/OAT] 111 | type=pseudoOATdriver.OAT 112 | Metadata/Kind = Temperature 113 | Metadata/Form = Air 114 | start=78 115 | rate=1 116 | jump=2 117 | Metadata/Description = Pseudo sensor generating simulated OAT 118 | 119 | [/monitoring/OAT/temperature] 120 | Metadata/Type = Sensor 121 | Metadata/Kind = Temperature 122 | Metadata/Form = Air 123 | 124 | 125 | -------------------------------------------------------------------------------- /smap-control-tutorial/startdrivers.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | 4 | inis = sys.argv[1:] 5 | 6 | processes = [] 7 | for ini in inis: 8 | command = "twistd --pidfile {0}.pid -n smap {0}.ini".format(ini) 9 | p = subprocess.Popen(command, shell=True) 10 | processes.append(p) 11 | # run smap query to see if the streams are registered 12 | # This is partly to get around the race condition arising when 13 | # a driver subscribes to the output of another 14 | # However, top handle the mutual dependence case, it really needs to 15 | # restart the services 16 | # Note that this encodes the uri of the archiver 17 | print subprocess.Popen('smap-query -u http://localhost:8079/api "select uuid"', shell=True, stdout=subprocess.PIPE).stdout.read() 18 | try: 19 | raw_input() 20 | except: 21 | for p in processes: 22 | print "killing",p 23 | p.terminate() 24 | -------------------------------------------------------------------------------- /smap-control-tutorial/temperatureDriver.py: -------------------------------------------------------------------------------- 1 | from smap import driver 2 | from smap.archiver.client import RepublishClient 3 | from smap.util import periodicSequentialCall 4 | import math 5 | 6 | class Temperature(driver.SmapDriver): 7 | def setup(self, opts): 8 | self.starttemp = float(opts.pop('start_temp','72')) 9 | self.rate = float(opts.pop('rate','1')) 10 | self.jump = float(opts.pop('jump','1')) 11 | self.curtemp = self.starttemp 12 | 13 | self.t = 1 14 | 15 | self.add_timeseries('/temperature','F',data_type='double') 16 | 17 | def start(self): 18 | periodicSequentialCall(self.read).start(self.rate) 19 | 20 | def read(self): 21 | self.t += 1 22 | self.curtemp = 78 + 10*math.sin(.01 * self.t) 23 | self.add('/temperature', self.curtemp) 24 | 25 | -------------------------------------------------------------------------------- /smap-control-tutorial/virtualthermostatDriver.py: -------------------------------------------------------------------------------- 1 | from smap import driver, actuate 2 | from smap.archiver.client import RepublishClient 3 | from smap.util import periodicSequentialCall 4 | 5 | class VirtualThermostat(driver.SmapDriver): 6 | def setup(self, opts): 7 | self.state = {'temp': 70, 8 | 'humidity': 50, 9 | 'hvac_state': 1, 10 | 'temp_heat': 70, 11 | 'temp_cool': 75, 12 | 'hold': 0, 13 | 'override': 0, 14 | 'hvac_mode': 1, 15 | 'fan_mode': 1, 16 | } 17 | 18 | self.readperiod = float(opts.get('ReadPeriod',3)) 19 | self.add_timeseries('/temp', 'F', data_type='long') 20 | self.add_timeseries('/humidity', '%RH', data_type='long') 21 | self.add_timeseries('/hvac_state', 'Mode', data_type='long') 22 | temp_heat = self.add_timeseries('/temp_heat', 'F', data_type='long') 23 | temp_cool = self.add_timeseries('/temp_cool', 'F', data_type='long') 24 | hold = self.add_timeseries('/hold', 'On/Off', data_type='long') 25 | override = self.add_timeseries('/override', 'On/Off', data_type='long') 26 | hvac_mode = self.add_timeseries('/hvac_mode', 'Mode', data_type='long') 27 | fan_mode = self.add_timeseries('/fan_mode', 'Mode', data_type='long') 28 | 29 | temp_heat.add_actuator(SetpointActuator(tstat=self, path='temp_heat', _range=(45, 95))) 30 | temp_cool.add_actuator(SetpointActuator(tstat=self, path='temp_cool', _range=(45, 95))) 31 | hold.add_actuator(OnOffActuator(tstat=self, path='hold')) 32 | override.add_actuator(OnOffActuator(tstat=self, path='override')) 33 | hvac_mode.add_actuator(ModeActuator(tstat=self, path='hvac_mode', states=[0,1,2,3])) 34 | fan_mode.add_actuator(OnOffActuator(tstat=self, path='fan_mode')) 35 | 36 | self.archiver_url = opts.pop('archiver_url','http://localhost:8079') 37 | self.heatSPpath = opts.pop('heatSPpath', '/scheduler/heatSetpoint') 38 | self.coolSPpath = opts.pop('coolSPpath', '/scheduler/coolSetpoint') 39 | # add mode 40 | self.siteid = opts.pop('siteid','') 41 | restriction = "Path = '{0}'".format(self.heatSPpath) 42 | if self.siteid : 43 | restriction = restriction + " and Metadata/Site/id = '{0}'".format(self.siteid) 44 | self.heatSPclient = RepublishClient(self.archiver_url, self.heatSPcb, restrict=restriction) 45 | restriction = "Path = '{0}'".format(self.coolSPpath) 46 | if self.siteid : 47 | restriction = restriction + " and Metadata/Site/id = '{0}'".format(self.siteid) 48 | self.coolSPclient = RepublishClient(self.archiver_url, self.coolSPcb, restrict=restriction) 49 | 50 | 51 | metadata_type = [ 52 | ('/temp','Sensor'), 53 | ('/humidity','Sensor'), 54 | ('/temp_heat','Reading'), 55 | ('/temp_heat_act','SP'), 56 | ('/temp_cool','Reading'), 57 | ('/temp_cool_act','SP'), 58 | ('/hold','Reading'), 59 | ('/hold_act','Command'), 60 | ('/override','Reading'), 61 | ('/override_act','Command'), 62 | ('/hvac_mode','Reading'), 63 | ('/hvac_mode_act','Command') 64 | ] 65 | for ts, tstype in metadata_type: 66 | self.set_metadata(ts,{'Metadata/Type':tstype}) 67 | 68 | def start(self): 69 | self.heatSPclient.connect() # activate subscription scheduler setpoints 70 | self.coolSPclient.connect() 71 | periodicSequentialCall(self.read).start(self.readperiod) 72 | 73 | def read(self): 74 | for k,v in self.state.iteritems(): 75 | self.add('/'+k, v) 76 | 77 | # Event handler for publication to heatSP stream 78 | def heatSPcb(self, _, data): 79 | # list of arrays of [time, val] 80 | mostrecent = data[-1][-1] 81 | self.heatSP = mostrecent[1] 82 | print "Set heating setpoint", self.heatSP 83 | self.state['temp_heat'] = self.heatSP 84 | 85 | def coolSPcb(self, _, data): 86 | # list of arrays of [time, val] 87 | mostrecent = data[-1][-1] 88 | self.coolSP = mostrecent[1] 89 | print "Set cooling setpoint", self.coolSP 90 | self.state['temp_cool'] = self.coolSP 91 | 92 | class VirtualThermostatActuator(actuate.SmapActuator): 93 | def __init__(self, **opts): 94 | self.tstat = opts.get('tstat') 95 | self.path = opts.get('path') 96 | 97 | class SetpointActuator(VirtualThermostatActuator, actuate.ContinuousIntegerActuator): 98 | def __init__(self, **opts): 99 | actuate.ContinuousIntegerActuator.__init__(self, opts['_range']) 100 | VirtualThermostatActuator.__init__(self, **opts) 101 | 102 | def get_state(self, request): 103 | return self.tstat.state[self.path] 104 | 105 | def set_state(self, request, state): 106 | self.tstat.state[self.path] = int(state) 107 | return self.tstat.state[self.path] 108 | 109 | class ModeActuator(VirtualThermostatActuator, actuate.NStateActuator): 110 | def __init__(self, **opts): 111 | actuate.NStateActuator.__init__(self, opts['states']) 112 | VirtualThermostatActuator.__init__(self, **opts) 113 | 114 | def get_state(self, request): 115 | return self.tstat.state[self.path] 116 | 117 | def set_state(self, request, state): 118 | self.tstat.state[self.path] = int(state) 119 | return self.tstat.state[self.path] 120 | 121 | class OnOffActuator(VirtualThermostatActuator, actuate.BinaryActuator): 122 | def __init__(self, **opts): 123 | actuate.BinaryActuator.__init__(self) 124 | VirtualThermostatActuator.__init__(self, **opts) 125 | 126 | def get_state(self, request): 127 | return self.tstat.state[self.path] 128 | 129 | def set_state(self, request, state): 130 | self.tstat.state[self.path] = int(state) 131 | return self.tstat.state[self.path] 132 | -------------------------------------------------------------------------------- /smap-control-tutorial/zoneControllerService.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2014, Regents of the University of California 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | - Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | - Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the 14 | distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 19 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 20 | THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 25 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 27 | OF THE POSSIBILITY OF SUCH DAMAGE. 28 | """ 29 | """ 30 | @author David Culler 31 | """ 32 | from smap import actuate, driver 33 | from smap.util import periodicSequentialCall 34 | from smap.archiver.client import RepublishClient 35 | from smap.archiver.client import SmapClient 36 | import time 37 | from pprint import pprint 38 | from smap.contrib import dtutil 39 | 40 | class ZoneController(driver.SmapDriver): 41 | def setup(self, opts): 42 | self.rate = float(opts.get('rate',10)) 43 | # Current state of the points 44 | self.heatSP=int(opts.get('defaultHeatSetpoint',68)) 45 | self.coolSP=int(opts.get('defaultCoolSetpoint',76)) 46 | 47 | self.therm_temp = 70 48 | 49 | self.trim = int(opts.get('trim',0)) # dummy zoneCtrl action 50 | 51 | # create timeseries for zone controller actions 52 | heatSetPoint = self.add_timeseries('/heatSetpoint', 'F', data_type='double') 53 | coolSetPoint = self.add_timeseries('/coolSetpoint', 'F', data_type='double') 54 | # add actuators to them 55 | heatSetPoint.add_actuator(setpointActuator(controller=self, range=(40,90))) 56 | coolSetPoint.add_actuator(setpointActuator(controller=self, range=(40,90))) 57 | 58 | # get master set point stream paths 59 | self.archiver_url = opts.get('archiver_url','http://localhost:8079') 60 | self.heatSPwhere = opts.get('heatSPwhere', '') 61 | self.coolSPwhere = opts.get('coolSPwhere', '') 62 | self.thermwhere = opts.get('thermwhere', '') 63 | self.tempwhere = opts.get('tempwhere', '') 64 | 65 | print "ZoneController: heat sp where = ", self.heatSPwhere 66 | print "ZoneController: cool sp where = ", self.coolSPwhere 67 | print "ZoneController: thermostat where = ", self.thermwhere 68 | print "ZoneController: temp sensor where = ", self.tempwhere 69 | 70 | self.client = SmapClient(self.archiver_url) 71 | 72 | self.heatSPclient = RepublishClient(self.archiver_url, self.heatSPcb, restrict=self.heatSPwhere) 73 | self.coolSPclient = RepublishClient(self.archiver_url, self.coolSPcb, restrict=self.coolSPwhere) 74 | #self.tempclient = RepublishClient(self.archiver_url, self.tempcb, restrict=self.tempwhere) 75 | self.thermclient = RepublishClient(self.archiver_url, self.thermcb, restrict=self.thermwhere) 76 | 77 | 78 | def start(self): 79 | print "zone controller start: ", self.rate 80 | self.heatSPclient.connect() # activate subscription scheduler setpoints 81 | self.coolSPclient.connect() 82 | #self.tempclient.connect() 83 | self.thermclient.connect() 84 | periodicSequentialCall(self.read).start(self.rate) 85 | 86 | def read(self): 87 | all_readings = self.client.latest(self.tempwhere) 88 | for p in all_readings: 89 | print '-'*20 90 | md = self.client.tags('uuid = "'+p['uuid']+'"')[0] 91 | print 'Room:', md['Metadata/Room'] 92 | print 'Reading:', p['Readings'][0][1] 93 | ts = dtutil.ts2dt(p['Readings'][0][0]/1000) 94 | print 'Time:', dtutil.strftime_tz(ts, tzstr='America/Los_Angeles') 95 | avg_room_temp = sum([x['Readings'][0][1] for x in all_readings]) / float(len(all_readings)) 96 | 97 | # get difference between avg room temperature and thermostat temperature 98 | new_diff = self.therm_temp - avg_room_temp 99 | 100 | # periodically update output streams. Here a bogus adjustment 101 | self.add('/heatSetpoint', self.heatSP + new_diff) 102 | self.add('/coolSetpoint', self.coolSP + new_diff) 103 | print "zone controller publish: ", self.heatSP, self.coolSP 104 | 105 | # Event handler for publication to heatSP stream 106 | def heatSPcb(self, _, data): 107 | # list of arrays of [time, val] 108 | print "ZoneController heatSPcb: ", data 109 | mostrecent = data[-1][-1] 110 | self.heatSP = mostrecent[1] 111 | 112 | def coolSPcb(self, _, data): 113 | # list of arrays of [time, val] 114 | print "ZoneController coolSPcb: ", data 115 | mostrecent = data[-1][-1] 116 | self.coolSP = mostrecent[1] 117 | 118 | def tempcb(self, _, data): 119 | # list of arrays of [time, val] 120 | print "ZoneController tempcb: ", data 121 | 122 | 123 | def thermcb(self, _, data): 124 | # list of arrays of [time, val] 125 | print "ZoneController thermcb: ", data 126 | self.therm_temp = data[-1][-1][1] 127 | 128 | 129 | 130 | class setpointActuator(actuate.ContinuousActuator): 131 | def __init__(self, **opts): 132 | actuate.ContinuousActuator.__init__(self, range=(40,100)) 133 | self.controller = opts.get('controller') 134 | 135 | def get_state(self, request): 136 | return self.controller.heatSP, self.controller.coolSP 137 | 138 | def set_state(self, request, val): 139 | print "set state", request, val 140 | # do something here 141 | return self.controller.heatSP, self.controller.coolSP 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /zone-controller-tutorial/README.md: -------------------------------------------------------------------------------- 1 | # Zone Controller Example 2 | 3 | Before reading this, please read the following sMAP wiki pages: 4 | 5 | * [Schedules](https://github.com/SoftwareDefinedBuildings/smap/wiki/Scheduler-Service) 6 | * [Zone Controllers](https://github.com/SoftwareDefinedBuildings/smap/wiki/Scheduler-Service) 7 | 8 | We consider the following small, 3-room, 2-zone building: 9 | 10 | ``` 11 | <---------------Zone 1-----------------><-----Zone 2-----> 12 | +--------------------------------------------------------+ 13 | | | | | 14 | | *sensor | *sensor | *sensor | 15 | | | | | 16 | | | | | 17 | | | | | 18 | | Room 1 | Room 2 | Room 3 | 19 | | | | | 20 | | | | | 21 | | | | | 22 | | | | | 23 | |*thermostat | |*thermostat | 24 | +--------------------------------------------------------+ 25 | ``` 26 | 27 | Each room has a temperature/humidity sensor, but only Rooms 1 and 3 have 28 | thermostats. Each room also has an independent lighting controller. 29 | 30 | We have a master schedule and a zone controller each for Zone 1 and Zone 2. We 31 | will have Zone 2 follow the master schedule with a simple trim of 5 degrees F, 32 | and Zone 1 will follow the master schedule, but trim by the difference between 33 | the average sensor temperature and the thermostat's reported temperature. 34 | -------------------------------------------------------------------------------- /zone-controller-tutorial/building.ini: -------------------------------------------------------------------------------- 1 | [report 1] 2 | ReportDeliveryLocation = http://localhost:8079/add/lVzBMDpnkXApJmpjUDSvm4ceGfpbrLLSd9cq 3 | 4 | [/] 5 | uuid = 90cc4cd1-3e8e-11e4-b536-6003089ed1d0 6 | Metadata/SourceName = Zone Controller Demo 7 | Metadata/Building = Demo Building 8 | Metadata/Site = a1a04b23-3e8e-11e4-9e19-6003089ed1d0 9 | 10 | [server] 11 | port = 8080 12 | 13 | ## Lighting Drivers ## 14 | [/buildinglighting] 15 | type = Collection 16 | Metadata/System = Lighting 17 | Metadata/Role = Building Lighting 18 | Metadata/Floor = 1 19 | Metadata/Name = 1st Floor Lighting Control 20 | 21 | [/buildinglighting/group0] 22 | type = smap.drivers.lights.virtuallight.VirtualLight 23 | Metadata/Group = 0 24 | Metadata/LightingZone = Room 1 25 | 26 | [/buildinglighting/group1] 27 | type = smap.drivers.lights.virtuallight.VirtualLight 28 | Metadata/Group = 1 29 | Metadata/LightingZone = Room 2 30 | 31 | [/buildinglighting/group2] 32 | type = smap.drivers.lights.virtuallight.VirtualLight 33 | Metadata/Group = 2 34 | Metadata/LightingZone = Room 3 35 | 36 | ## Thermostat Drivers ## 37 | [/buildinghvac] 38 | type = Collection 39 | Metadata/System = HVAC 40 | Metadata/Role = Building HVAC 41 | Metadata/Floor = 1 42 | Metadata/Name = 1st Floor Thermostats 43 | 44 | [/buildinghvac/thermostat0] 45 | type = smap.drivers.thermostats.virtualthermostat.VirtualThermostat 46 | Metadata/Room = 1 47 | Metadata/HVACZone = Zone 1 48 | temp_heat = Metadata/System = 'HVAC' and Metadata/HVACZone = 'Zone 1' and Metadata/Description = 'Zone Heating setpoint' 49 | temp_cool = Metadata/System = 'HVAC' and Metadata/HVACZone = 'Zone 1' and Metadata/Description = 'Zone Cooling setpoint' 50 | 51 | [/buildinghvac/thermostat1] 52 | type = smap.drivers.thermostats.virtualthermostat.VirtualThermostat 53 | Metadata/Room = 3 54 | Metadata/HVACZone = Zone 2 55 | temp_heat = Metadata/System = 'HVAC' and Metadata/HVACZone = 'Zone 2' and Metadata/Description = 'Zone Heating setpoint' 56 | temp_cool = Metadata/System = 'HVAC' and Metadata/HVACZone = 'Zone 2' and Metadata/Description = 'Zone Cooling setpoint' 57 | 58 | ## Sensor Drivers ## 59 | [/monitoring] 60 | type = Collection 61 | Metadata/System = Monitoring 62 | Metadata/Role = Monitoring 63 | Metadata/Floor = 1 64 | Metadata/Name = 1st Floor Sensors 65 | 66 | [/monitoring/airtemphumidity0] 67 | type = smap.drivers.sensors.virtualATHsensor.VirtualATHSensor 68 | initialtemp = 68 69 | initialhumidity = 38 70 | Metadata/Type = Sensor 71 | Metadata/Room = 1 72 | Metadata/HVACZone = Zone 1 73 | 74 | [/monitoring/airtemphumidity1] 75 | type = smap.drivers.sensors.virtualATHsensor.VirtualATHSensor 76 | initialtemp = 69 77 | initialhumidity = 39 78 | Metadata/Type = Sensor 79 | Metadata/Room = 2 80 | Metadata/HVACZone = Zone 1 81 | 82 | [/monitoring/airtemphumidity2] 83 | type = smap.drivers.sensors.virtualATHsensor.VirtualATHSensor 84 | initialtemp = 70 85 | initialhumidity = 40 86 | Metadata/Type = Sensor 87 | Metadata/Room = 3 88 | Metadata/HVACZone = Zone 2 89 | 90 | ## Zone Controller ## 91 | [/zonecontroller1] 92 | type = zone1controller.AvgSensorFollowMaster 93 | subscribe/temp_sensor = Metadata/System = 'Monitoring' and Metadata/HVACZone = 'Zone 1' and Metadata/Type = 'Sensor' and Properties/UnitofMeasure = 'F' 94 | subscribe/thermostat_temp = Metadata/System = 'HVAC' and Metadata/HVACZone = 'Zone 1' and Metadata/Type = 'Sensor' and Properties/UnitofMeasure = 'F' 95 | subscribe/temp_heat = Metadata/System = 'Schedule' and Metadata/Description = 'Master Heating setpoint' 96 | subscribe/temp_cool = Metadata/System = 'Schedule' and Metadata/Description = 'Master Cooling setpoint' 97 | archiver = http://localhost:8079 98 | synchronous = True 99 | Metadata/HVACZone = Zone 1 100 | 101 | [/zonecontroller1/temp_heat] 102 | Metadata/Description = Zone Heating setpoint 103 | Metadata/System = HVAC 104 | Metadata/Type = Setpoint 105 | 106 | [/zonecontroller1/temp_cool] 107 | Metadata/Description = Zone Cooling setpoint 108 | Metadata/System = HVAC 109 | Metadata/Type = Setpoint 110 | 111 | [/zonecontroller2] 112 | type = zone2controller.FollowMaster 113 | subscribe/temp_heat = Metadata/System = 'Schedule' and Metadata/Description = 'Master Heating setpoint' 114 | subscribe/temp_cool = Metadata/System = 'Schedule' and Metadata/Description = 'Master Cooling setpoint' 115 | archiver = http://localhost:8079 116 | synchronous = True 117 | Metadata/HVACZone = Zone 2 118 | 119 | [/zonecontroller2/temp_heat] 120 | Metadata/Description = Zone Heating setpoint 121 | Metadata/System = HVAC 122 | Metadata/Type = Setpoint 123 | 124 | [/zonecontroller2/temp_cool] 125 | Metadata/Description = Zone Cooling setpoint 126 | Metadata/System = HVAC 127 | Metadata/Type = Setpoint 128 | 129 | ## Master Scheduler ## 130 | [/masterscheduler] 131 | type = smap.services.scheduler.Scheduler 132 | pollrate = 1 133 | publishrate = 1 134 | source = file:///schedules.json 135 | 136 | [/masterscheduler/temp_heat] 137 | Metadata/Description = Master Heating setpoint 138 | Metadata/System = Schedule 139 | Metadata/Type = Setpoint 140 | 141 | [/masterscheduler/temp_cool] 142 | Metadata/Description = Master Cooling setpoint 143 | Metadata/System = Schedule 144 | Metadata/Type = Setpoint 145 | 146 | [/masterscheduler/on] 147 | Metadata/Description = Master Lighting control 148 | Metadata/System = Schedule 149 | Metadata/Type = Command 150 | -------------------------------------------------------------------------------- /zone-controller-tutorial/schedules.json: -------------------------------------------------------------------------------- 1 | { 2 | "master_schedule": { 3 | "fri": "weekday", 4 | "mon": "weekday", 5 | "sat": "weekend", 6 | "sun": "weekend", 7 | "thu": "weekday", 8 | "tue": "weekday", 9 | "wed": "weekday" 10 | }, 11 | "schedules": [ 12 | { 13 | "color": "#EDF1FA", 14 | "name": "weekday", 15 | "periods": [ 16 | { 17 | "name": "morning", 18 | "points": [ 19 | { 20 | "path": "temp_heat", 21 | "value": 72 22 | }, 23 | { 24 | "path": "temp_cool", 25 | "value": 83 26 | } 27 | ], 28 | "start": "7:30" 29 | }, 30 | { 31 | "name": "afternoon", 32 | "points": [ 33 | { 34 | "path": "temp_heat", 35 | "value": 70 36 | }, 37 | { 38 | "path": "temp_cool", 39 | "value": 80 40 | } 41 | ], 42 | "start": "13:30" 43 | }, 44 | { 45 | "name": "evening", 46 | "points": [ 47 | { 48 | "path": "temp_heat", 49 | "value": 50 50 | }, 51 | { 52 | "path": "temp_cool", 53 | "value": 90 54 | } 55 | ], 56 | "start": "18:30" 57 | } 58 | ] 59 | }, 60 | { 61 | "color": "#FAF6ED", 62 | "name": "weekend", 63 | "periods": [ 64 | { 65 | "name": "morning", 66 | "points": [ 67 | { 68 | "path": "temp_heat", 69 | "value": 65 70 | }, 71 | { 72 | "path": "temp_cool", 73 | "value": 85 74 | } 75 | ], 76 | "start": "09:30" 77 | }, 78 | { 79 | "name": "afternoon", 80 | "points": [ 81 | { 82 | "path": "temp_heat", 83 | "value": 70 84 | }, 85 | { 86 | "path": "temp_cool", 87 | "value": 80 88 | } 89 | ], 90 | "start": "17:30" 91 | }, 92 | { 93 | "name": "evening", 94 | "points": [ 95 | { 96 | "path": "temp_heat", 97 | "value": 50 98 | }, 99 | { 100 | "path": "temp_cool", 101 | "value": 90 102 | } 103 | ], 104 | "start": "21:00" 105 | } 106 | ] 107 | } 108 | ] 109 | } 110 | -------------------------------------------------------------------------------- /zone-controller-tutorial/zone1controller.py: -------------------------------------------------------------------------------- 1 | from smap.services.zonecontroller import ZoneController 2 | 3 | class AvgSensorFollowMaster(ZoneController): 4 | def setup(self, opts): 5 | ZoneController.setup(self, opts) 6 | # we want to publish the heat/cool setpoints 7 | self.add_timeseries('/temp_heat','F',data_type='double') 8 | self.add_timeseries('/temp_cool','F',data_type='double') 9 | # we want to write a special averaging function to handle the temperature sensors, so 10 | # we use add_callback to run it automatically when new sensor data comes in 11 | self.add_callback('temp_sensor', self.avg_temp, opts.get('subscribe/temp_sensor')) 12 | 13 | def step(self): 14 | # adjust heat/cool setpoints by the difference between the thermostat temperature and the 15 | # average room setpoint 16 | new_diff = self.points['thermostat_temp'] - self.points['avg_temp'] 17 | self.points['temp_heat'] += new_diff 18 | self.points['temp_cool'] += new_diff 19 | self.add('/temp_heat', float(self.points['temp_heat'])) 20 | self.add('/temp_cool', float(self.points['temp_cool'])) 21 | 22 | def avg_temp(self, point, uuids, data): 23 | # averages the readings 24 | self.points['avg_temp'] = sum(map(lambda x: x[-1][1], data)) / float(len(data)) 25 | -------------------------------------------------------------------------------- /zone-controller-tutorial/zone2controller.py: -------------------------------------------------------------------------------- 1 | from smap.services.zonecontroller import ZoneController 2 | 3 | class FollowMaster(ZoneController): 4 | def setup(self, opts): 5 | ZoneController.setup(self, opts) 6 | self.add_timeseries('/temp_heat','F',data_type='double') 7 | self.add_timeseries('/temp_cool','F',data_type='double') 8 | 9 | def step(self): 10 | """ 11 | self.points contains the most recent advertised values 12 | for 'temp_haet' and 'temp_cool' from the master scheduler. 13 | We add 5 to these, and then republish to our end points 14 | """ 15 | self.add('/temp_heat', float(self.points['temp_heat']) + 5) 16 | self.add('/temp_cool', float(self.points['temp_cool']) + 5) 17 | --------------------------------------------------------------------------------