Browse Source

Reimport projects into this one

master
Lucas Verney 6 years ago
parent
commit
fda74508a7
No known key found for this signature in database
GPG Key ID: 75B45CF41F334690
  1. 34
      CitizenWatt-Base/Makefile
  2. 39
      CitizenWatt-Base/README.md
  3. 3624
      CitizenWatt-Base/bottle.py
  4. 163
      CitizenWatt-Base/bottle_sqlalchemy.py
  5. 191
      CitizenWatt-Base/bottlesession.py
  6. 2
      CitizenWatt-Base/libcitizenwatt/__init__.py
  7. 316
      CitizenWatt-Base/libcitizenwatt/cache.py
  8. 80
      CitizenWatt-Base/libcitizenwatt/config.py
  9. 65
      CitizenWatt-Base/libcitizenwatt/database.py
  10. 108
      CitizenWatt-Base/libcitizenwatt/tools.py
  11. 1
      CitizenWatt-Base/post_update.sh
  12. 109
      CitizenWatt-Base/process.py
  13. 109
      CitizenWatt-Base/receive.cpp
  14. 20
      CitizenWatt-Base/startup.sh
  15. 425
      CitizenWatt-Base/static/css/normalize.css
  16. 896
      CitizenWatt-Base/static/css/style.css
  17. BIN
      CitizenWatt-Base/static/font/OpenSans-Bold.ttf
  18. BIN
      CitizenWatt-Base/static/font/OpenSans-BoldItalic.ttf
  19. BIN
      CitizenWatt-Base/static/font/OpenSans-Italic.ttf
  20. BIN
      CitizenWatt-Base/static/font/OpenSans-Light.ttf
  21. BIN
      CitizenWatt-Base/static/font/OpenSans-Regular.ttf
  22. 118
      CitizenWatt-Base/static/img/bill.svg
  23. 88
      CitizenWatt-Base/static/img/community.svg
  24. 99
      CitizenWatt-Base/static/img/contact.svg
  25. 97
      CitizenWatt-Base/static/img/data.svg
  26. 100
      CitizenWatt-Base/static/img/day.svg
  27. 83
      CitizenWatt-Base/static/img/help.svg
  28. 111
      CitizenWatt-Base/static/img/install.svg
  29. 130
      CitizenWatt-Base/static/img/loading.svg
  30. 124
      CitizenWatt-Base/static/img/loading_simple.svg
  31. 83
      CitizenWatt-Base/static/img/login.svg
  32. BIN
      CitizenWatt-Base/static/img/logo.png
  33. 94
      CitizenWatt-Base/static/img/logout.svg
  34. 100
      CitizenWatt-Base/static/img/month.svg
  35. 195
      CitizenWatt-Base/static/img/moon.svg
  36. 89
      CitizenWatt-Base/static/img/more.svg
  37. 87
      CitizenWatt-Base/static/img/progress.svg
  38. 113
      CitizenWatt-Base/static/img/results.svg
  39. 83
      CitizenWatt-Base/static/img/sensor.svg
  40. 107
      CitizenWatt-Base/static/img/small-data.svg
  41. 181
      CitizenWatt-Base/static/img/sun.svg
  42. 94
      CitizenWatt-Base/static/img/target-no.svg
  43. 89
      CitizenWatt-Base/static/img/target-ok.svg
  44. 132
      CitizenWatt-Base/static/img/target-wip.svg
  45. 95
      CitizenWatt-Base/static/img/target.svg
  46. 89
      CitizenWatt-Base/static/img/tick.svg
  47. 90
      CitizenWatt-Base/static/img/user.svg
  48. 100
      CitizenWatt-Base/static/img/week.svg
  49. 112
      CitizenWatt-Base/static/img/wiki.svg
  50. 206
      CitizenWatt-Base/static/js/conso/App.js
  51. 7
      CitizenWatt-Base/static/js/conso/Config.js
  52. 89
      CitizenWatt-Base/static/js/conso/DataProvider.js
  53. 345
      CitizenWatt-Base/static/js/conso/Graph.js
  54. 45
      CitizenWatt-Base/static/js/conso/HashManager.js
  55. 242
      CitizenWatt-Base/static/js/conso/Menu.js
  56. 28
      CitizenWatt-Base/static/js/conso/RateDisplay.js
  57. 16
      CitizenWatt-Base/static/js/conso/tail.js
  58. 212
      CitizenWatt-Base/static/js/dateutils.js
  59. 29
      CitizenWatt-Base/static/js/target.js
  60. 15
      CitizenWatt-Base/static/js/utils.js
  61. 9
      CitizenWatt-Base/system/README.md
  62. 31
      CitizenWatt-Base/system/citizenwatt.sh
  63. 23
      CitizenWatt-Base/system/cleanup_raspbian.sh
  64. 35
      CitizenWatt-Base/system/install_citizenwatt.sh
  65. 17
      CitizenWatt-Base/system/install_python34.sh
  66. 14
      CitizenWatt-Base/system/install_rf24.sh
  67. 34
      CitizenWatt-Base/system/supervisor_citizenwatt.conf
  68. 1
      CitizenWatt-Base/tests/libcitizenwatt
  69. 73
      CitizenWatt-Base/tests/test_process.py
  70. 48
      CitizenWatt-Base/tests/tests.js
  71. 1
      CitizenWatt-Base/updater.sh
  72. 34
      CitizenWatt-Base/views/_begin.tpl
  73. 27
      CitizenWatt-Base/views/_end.tpl
  74. 13
      CitizenWatt-Base/views/community.tpl
  75. 59
      CitizenWatt-Base/views/conso.tpl
  76. 32
      CitizenWatt-Base/views/help.tpl
  77. 46
      CitizenWatt-Base/views/index.tpl
  78. 80
      CitizenWatt-Base/views/install.tpl
  79. 32
      CitizenWatt-Base/views/login.tpl
  80. 111
      CitizenWatt-Base/views/settings.tpl
  81. 987
      CitizenWatt-Base/visu.py
  82. BIN
      CitizenWatt-PiBoard/2017-02-15-201954.png
  83. BIN
      CitizenWatt-PiBoard/2017-02-15-202006.png
  84. 339
      CitizenWatt-PiBoard/LICENSE
  85. 1858
      CitizenWatt-PiBoard/PiBoard.brd
  86. 4477
      CitizenWatt-PiBoard/PiBoard.sch
  87. 4
      CitizenWatt-PiBoard/README.md
  88. 40
      CitizenWatt-sensor/Hardware/.gitignore
  89. 7225
      CitizenWatt-sensor/Hardware/CitizenBoard.brd
  90. 16977
      CitizenWatt-sensor/Hardware/CitizenBoard.sch
  91. 159
      CitizenWatt-sensor/Hardware/silkscreen3bold_standalone.svg
  92. 339
      CitizenWatt-sensor/LICENSE
  93. 8
      CitizenWatt-sensor/README.md
  94. 30
      CitizenWatt-sensor/Software/sensor/printf.h
  95. 316
      CitizenWatt-sensor/Software/sensor/sensor.ino

34
CitizenWatt-Base/Makefile

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
#############################################################################
#
# Makefile for CitizenWatt install on Raspberry Pi
#
# License: GPL (General Public License)
# Author: AlexFaraino
# Date: 2014/10/20 (v1.0)
#
# Description:
# ------------
# You can change the install directory by editing the prefix line
#
prefix=$(DESTDIR)/opt/citizenwatt
files=`ls | grep -v debian`
sup_prefix=$(DESTDIR)/etc/supervisor/conf.d/
# The recommended compiler flags for the Raspberry Pi
CCFLAGS=-Wall -Ofast -mfpu=vfp -mfloat-abi=hard -march=armv6zk -mtune=arm1176jzf-s
all: receive
receive: receive.cpp
g++ ${CCFLAGS} -lrf24 $@.cpp -o $@
clean:
rm -rf receive
install: all
test -d $(prefix) || mkdir -p $(prefix)
test -d $(sup_prefix) || mkdir -p $(sup_prefix)
cp -r $(files) $(prefix)/
cp system/supervisor_citizenwatt.conf $(sup_prefix)/citizenwatt.conf
.PHONY: install

39
CitizenWatt-Base/README.md

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
## Package needed
* sqlalchemy
* cherrypy
* numpy
* pycrypto
* psycopg2 for communication with the PostgreSQL database
## API
* /api/sensors
* Returns all the available sensors with their types
* /api/sensors/<id:int>
* Returns the infos for the specified sensor.
* /api/types
* Returns all the available measure types
* /api/time
* Returns the current timestamp of the server side.
* /api/energy_providers
* Returns all available energy providers
* /api/energy_providers/<current|<int>>
* Returns the targeted energy provider
* /api/<sensor:int>/get/watts/by_id/<nb:int>
* Get measure with id nb
* Get measure nth to last measure if nb < 0 (behaviour of Python lists)
* /api/<sensor:int>/get/[watts|kwatthours|euros]/by_id/<nb1:int>/<nb2:int>
* Get all the measures with id between nb1 and nb2 (nb1 < nb2)
* Get all the measures between nb1 and nb2 starting from the end if nb1, nb2 < 0 (behaviour of Python lists)
* Get the energy / cost associated with these measures if kwatthours or euros is specified
* /api/<sensor:int>/get/watts/by_time/<time:int>
* Idem as above, but with timestamps
* /api/<provider:re:current|\d>/watt_to_euros/<tarif:re:night|day>/<consumption:int>
* Returns the price associated to the consumption (in kWh) for the specified provider
* /api/<sensor:int>/get/[watts|kwatthours|euros]/by_time/<time1:int>/<time2:int>/<timestep:int>
* Idem as above, but with timestamps
* idem avec id
* idem with ids
step > 0

3624
CitizenWatt-Base/bottle.py

File diff suppressed because it is too large Load Diff

163
CitizenWatt-Base/bottle_sqlalchemy.py

@ -0,0 +1,163 @@ @@ -0,0 +1,163 @@
'''
This bottle-sqlalchemy plugin integrates SQLAlchemy with your Bottle
application. It connects to a database at the beginning of a request,
passes the database handle to the route callback and closes the connection
afterwards.
The plugin inject an argument to all route callbacks that require a `db`
keyword.
Usage Example::
import bottle
from bottle import HTTPError
from bottle.ext import sqlalchemy
from sqlalchemy import create_engine, Column, Integer, Sequence, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
engine = create_engine('sqlite:///:memory:', echo=True)
app = bottle.Bottle()
plugin = sqlalchemy.Plugin(engine, Base.metadata, create=True)
app.install(plugin)
class Entity(Base):
__tablename__ = 'entity'
id = Column(Integer, Sequence('id_seq'), primary_key=True)
name = Column(String(50))
def __init__(self, name):
self.name = name
def __repr__(self):
return "<Entity('%d', '%s')>" % (self.id, self.name)
@app.get('/:name')
def show(name, db):
entity = db.query(Entity).filter_by(name=name).first()
if entity:
return {'id': entity.id, 'name': entity.name}
return HTTPError(404, 'Entity not found.')
@app.put('/:name')
def put_name(name, db):
entity = Entity(name)
db.add(entity)
It is up to you create engine and metadata, because SQLAlchemy has
a lot of options to do it. The plugin just handles the SQLAlchemy
session.
Copyright (c) 2011-2012, Iuri de Silvio
License: MIT (see LICENSE for details)
'''
import inspect
import bottle
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.scoping import ScopedSession
# PluginError is defined to bottle >= 0.10
if not hasattr(bottle, 'PluginError'):
class PluginError(bottle.BottleException):
pass
bottle.PluginError = PluginError
class SQLAlchemyPlugin(object):
name = 'sqlalchemy'
api = 2
def __init__(self, engine, metadata=None,
keyword='db', commit=True, create=False, use_kwargs=False, create_session=None):
'''
:param engine: SQLAlchemy engine created with `create_engine` function
:param metadata: SQLAlchemy metadata. It is required only if `create=True`
:param keyword: Keyword used to inject session database in a route
:param create: If it is true, execute `metadata.create_all(engine)`
when plugin is applied
:param commit: If it is true, commit changes after route is executed.
:param use_kwargs: plugin inject session database even if it is not
explicitly defined, using **kwargs argument if defined.
:param create_session: SQLAlchemy session maker created with the
'sessionmaker' function. Will create its own if undefined.
'''
self.engine = engine
if create_session is None:
create_session = sessionmaker()
self.create_session = create_session
self.metadata = metadata
self.keyword = keyword
self.create = create
self.commit = commit
self.use_kwargs = use_kwargs
def setup(self, app):
''' Make sure that other installed plugins don't affect the same
keyword argument and check if metadata is available.'''
for other in app.plugins:
if not isinstance(other, SQLAlchemyPlugin):
continue
if other.keyword == self.keyword:
raise bottle.PluginError("Found another SQLAlchemy plugin with "\
"conflicting settings (non-unique keyword).")
elif other.name == self.name:
self.name += '_%s' % self.keyword
if self.create and not self.metadata:
raise bottle.PluginError('Define metadata value to create database.')
def apply(self, callback, route):
# hack to support bottle v0.9.x
if bottle.__version__.startswith('0.9'):
config = route['config']
_callback = route['callback']
else:
config = route.config
_callback = route.callback
if "sqlalchemy" in config: # support for configuration before `ConfigDict` namespaces
g = lambda key, default: config.get('sqlalchemy', {}).get(key, default)
else:
g = lambda key, default: config.get('sqlalchemy.' + key, default)
keyword = g('keyword', self.keyword)
create = g('create', self.create)
commit = g('commit', self.commit)
use_kwargs = g('use_kwargs', self.use_kwargs)
argspec = inspect.getargspec(_callback)
if not ((use_kwargs and argspec.keywords) or keyword in argspec.args):
return callback
if create:
self.metadata.create_all(self.engine)
def wrapper(*args, **kwargs):
kwargs[keyword] = session = self.create_session(bind=self.engine)
try:
rv = callback(*args, **kwargs)
if commit:
session.commit()
except (SQLAlchemyError, bottle.HTTPError):
session.rollback()
raise
except bottle.HTTPResponse:
if commit:
session.commit()
raise
finally:
if isinstance(self.create_session, ScopedSession):
self.create_session.remove()
else:
session.close()
return rv
return wrapper
Plugin = SQLAlchemyPlugin

191
CitizenWatt-Base/bottlesession.py

@ -0,0 +1,191 @@ @@ -0,0 +1,191 @@
#!/usr/bin/env python
#
# Bottle session manager. See README for full documentation.
#
# Written by: Sean Reifschneider <jafo@tummy.com>
#
# License: 3-clause BSD
from __future__ import with_statement
import bottle
import time
def authenticator(session_manager, login_url='/auth/login'):
'''Create an authenticator decorator.
:param session_manager: A session manager class to be used for storing
and retrieving session data. Probably based on
:class:`BaseSession`.
:param login_url: The URL to redirect to if a login is required.
(default: ``'/auth/login'``).
'''
def valid_user(login_url=login_url):
def decorator(handler):
import functools
@functools.wraps(handler)
def check_auth(*args, **kwargs):
try:
data = session_manager.get_session()
if not data['valid']:
raise KeyError('Invalid login')
except (KeyError, TypeError):
bottle.response.set_cookie(
'validuserloginredirect',
bottle.request.fullpath, path='/',
expires=(int(time.time()) + 3600))
bottle.redirect(login_url)
# set environment
if data.get('name'):
bottle.request.environ['REMOTE_USER'] = data['name']
return handler(*args, **kwargs)
return check_auth
return decorator
return(valid_user)
import pickle
import os
import uuid
class BaseSession(object):
'''Base class which implements some of the basic functionality required for
session managers. Cannot be used directly.
:param cookie_expires: Expiration time of session ID cookie, either `None`
if the cookie is not to expire, a number of seconds in the future,
or a datetime object. (default: 30 days)
'''
def __init__(self, cookie_expires=86400 * 30):
self.cookie_expires = cookie_expires
def load(self, sessionid):
raise NotImplementedError
def save(self, sessionid, data):
raise NotImplementedError
def make_session_id(self):
return str(uuid.uuid4())
def allocate_new_session_id(self):
# retry allocating a unique sessionid
for i in range(100):
sessionid = self.make_session_id()
if not self.load(sessionid):
return sessionid
raise ValueError('Unable to allocate unique session')
def get_session(self):
# get existing or create new session identifier
sessionid = bottle.request.cookies.get('sessionid')
if not sessionid:
sessionid = self.allocate_new_session_id()
bottle.response.set_cookie(
'sessionid', sessionid, path='/',
expires=(int(time.time()) + self.cookie_expires))
# load existing or create new session
data = self.load(sessionid)
if not data:
data = {'sessionid': sessionid, 'valid': False}
self.save(data)
return data
class PickleSession(BaseSession):
'''Class which stores session information in the file-system.
:param session_dir: Directory that session information is stored in.
(default: ``'/tmp'``).
'''
def __init__(self, session_dir='/tmp', *args, **kwargs):
super(PickleSession, self).__init__(*args, **kwargs)
self.session_dir = session_dir
def load(self, sessionid):
filename = os.path.join(self.session_dir, 'session-%s' % sessionid)
if not os.path.exists(filename):
return None
with open(filename, 'rb') as fp:
session = pickle.load(fp)
return session
def save(self, data):
sessionid = data['sessionid']
fileName = os.path.join(self.session_dir, 'session-%s' % sessionid)
tmpName = fileName + '.' + str(uuid.uuid4())
with open(tmpName, 'wb') as fp:
self.session = pickle.dump(data, fp)
os.rename(tmpName, fileName)
class CookieSession(BaseSession):
'''Session manager class which stores session in a signed browser cookie.
:param cookie_name: Name of the cookie to store the session in.
(default: ``session_data``)
:param secret: Secret to be used for "secure cookie". If ``None``,
a random secret will be generated and written to a temporary
file for future use. This may not be suitable for systems which
have untrusted users on it. (default: ``None``)
:param secret_file: File to read the secret from. If ``secret`` is
``None`` and ``secret_file`` is set, the first line of this file
is read, and stripped, to produce the secret.
'''
def __init__(
self, secret=None, secret_file=None, cookie_name='session_data',
*args, **kwargs):
super(CookieSession, self).__init__(*args, **kwargs)
self.cookie_name = cookie_name
if not secret and secret_file is not None:
with open(secret_file, 'r') as fp:
secret = fp.readline().strip()
if not secret:
import string
import random
import tempfile
import sys
tmpfilename = os.path.join(
tempfile.gettempdir(),
'%s.secret' % os.path.basename(sys.argv[0]))
if os.path.exists(tmpfilename):
with open(tmpfilename, 'r') as fp:
secret = fp.readline().strip()
else:
# save off a secret to a tmp file
secret = ''.join([
random.choice(string.ascii_letters)
for x in range(32)])
old_umask = os.umask(0o77)
with open(tmpfilename, 'w') as fp:
fp.write(secret)
os.umask(old_umask)
self.secret = secret
def load(self, sessionid):
cookie = bottle.request.get_cookie(
self.cookie_name,
secret=self.secret)
if cookie is None:
return {}
return pickle.loads(cookie)
def save(self, data):
bottle.response.set_cookie(
self.cookie_name, pickle.dumps(data), secret=self.secret,
path='/', expires=int(time.time()) + self.cookie_expires,
secure=True, httponly=True)

2
CitizenWatt-Base/libcitizenwatt/__init__.py

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
#!/usr/bin/env python2
# -*- coding: utf-8 -*-

316
CitizenWatt-Base/libcitizenwatt/cache.py

@ -0,0 +1,316 @@ @@ -0,0 +1,316 @@
#!/usr/bin/env python3
import bisect
import datetime
import json
import numpy
import redis
from libcitizenwatt import database
from libcitizenwatt import tools
from sqlalchemy import asc, desc
from libcitizenwatt.config import Config
config = Config()
def do_cache_ids(sensor, watt_euros, id1, id2, db, force_refresh=False):
"""
Computes the cache (if needed) for the API call
/api/<sensor:int>/get/<watt_euros:re:watts|kwatthours|euros>/by_id/<id1:int>/<id2:int>
Returns the stored (or computed) data or None if parameters are invalid.
"""
r = redis.Redis(decode_responses=True)
if not force_refresh:
data = r.get(watt_euros + "_" + str(sensor) + "_" + "by_id" + "_" +
str(id1) + "_" + str(id2))
if data:
# If found in cache, return it
return json.loads(data)
if id1 >= 0 and id2 >= 0 and id2 >= id1:
data = (db.query(database.Measures)
.filter(database.Measures.sensor_id == sensor,
database.Measures.id >= id1,
database.Measures.id < id2)
.order_by(asc(database.Measures.timestamp))
.all())
elif id1 <= 0 and id2 <= 0 and id2 >= id1:
data = (db.query(database.Measures)
.filter_by(sensor_id=sensor)
.order_by(desc(database.Measures.timestamp))
.slice(-id2, -id1)
.all())
data.reverse()
else:
return None
if not data:
data = None
else:
time1 = data[0].timestamp
time2 = data[-1].timestamp
if watt_euros == 'kwatthours' or watt_euros == 'euros':
data = tools.energy(data)
if watt_euros == 'euros':
if data["night_rate"] != 0:
night_rate = tools.watt_euros(0,
'night',
data['night_rate'],
db)
else:
night_rate = 0
if data["day_rate"] != 0:
day_rate = tools.watt_euros(0,
'day',
data['day_rate'],
db)
else:
day_rate = 0
data = {"value": night_rate + day_rate}
else:
data = tools.to_dict(data)
# Store in cache
r.set(watt_euros + "_" + str(sensor) + "_" + "by_id" + "_" +
str(id1) + "_" + str(id2),
json.dumps(data),
time2 - time1)
return data
def do_cache_group_id(sensor, watt_euros, id1, id2, step, db,
timestep=config.get("default_timestep"),
force_refresh=False):
"""
Computes the cache (if needed) for the API call
/api/<sensor:int>/get/<watt_euros:re:watts|kwatthours|euros>/by_id/<id1:int>/<id2:int>/<step:int>
Returns the stored (or computed) data.
"""
r = redis.Redis(decode_responses=True)
if not force_refresh:
data = r.get(watt_euros + "_" + str(sensor) + "_" + "by_id" + "_" +
str(id1) + "_" + str(id2) + "_" +
str(step) + "_" + str(timestep))
if data:
# If found in cache, return it
return json.loads(data)
steps = [i for i in range(id1, id2, step)]
steps.append(id2)
if id1 >= 0 and id2 >= 0 and id2 >= id1:
data = (db.query(database.Measures)
.filter(database.Measures.sensor_id == sensor,
database.Measures.id >= id1,
database.Measures.id < id2)
.order_by(asc(database.Measures.timestamp))
.all())
elif id1 <= 0 and id2 <= 0 and id2 >= id1:
data = (db.query(database.Measures)
.filter_by(sensor_id=sensor)
.order_by(desc(database.Measures.timestamp))
.slice(-id2, -id1)
.all())
data.reverse()
else:
raise ValueError
time2 = None
if not data:
data = [None for i in range(len(steps) - 1)]
else:
time1 = data[0].timestamp
time2 = data[-1].timestamp
data_dict = tools.to_dict(data)
tmp = [[] for i in range(len(steps) - 1)]
for i in data_dict:
tmp[bisect.bisect_left(steps, i["id"]) - 1].append(i)
data = []
for i in tmp:
if len(i) == 0:
data.append(None)
continue
energy = tools.energy(i)
if watt_euros == "watts":
tmp_data = {"value": energy["value"] / (step * timestep) * 1000 * 3600,
"day_rate": energy["day_rate"] / (step * timestep) * 1000 * 3600,
"night_rate": energy["night_rate"] / (step * timestep) * 1000 * 3600}
elif watt_euros == 'kwatthours':
tmp_data = energy
elif watt_euros == 'euros':
if energy["night_rate"] != 0:
night_rate = tools.watt_euros(0,
'night',
energy['night_rate'],
db)
else:
night_rate = 0
if energy["day_rate"] != 0:
day_rate = tools.watt_euros(0,
'day',
energy['day_rate'],
db)
else:
day_rate = 0
tmp_data = {"value": night_rate + day_rate}
data.append(tmp_data)
if len(data) == 0:
data = None
if time2 is not None:
# Store in cache
if time2 < datetime.datetime.now().timestamp():
# If new measures are to come, short lifetime (basically timestep)
r.set(watt_euros + "_" + str(sensor) + "_" + "by_id" + "_" +
str(id1) + "_" + str(id2) + "_" +
str(step) + "_" + str(timestep),
json.dumps(data),
timestep)
else:
# Else, store for a greater lifetime (basically time2 - time1)
r.set(watt_euros + "_" + str(sensor) + "_" + "by_id" + "_" +
str(id1) + "_" + str(id2) + "_" +
str(step) + "_" + str(timestep),
json.dumps(data),
time2 - time1)
return data
def do_cache_times(sensor, watt_euros, time1, time2, db, force_refresh=False):
"""
Computes the cache (if needed) for the API call
/api/<sensor:int>/get/<watt_euros:re:watts|kwatthours|euros>/by_time/<time1:float>/<time2:float>
Returns the stored (or computed) data.
"""
r = redis.Redis(decode_responses=True)
if not force_refresh:
data = r.get(watt_euros + "_" + str(sensor) + "_" + "by_time" + "_" +
str(time1) + "_" + str(time2))
if data:
# If found in cache, return it
return json.loads(data)
data = (db.query(database.Measures)
.filter(database.Measures.sensor_id == sensor,
database.Measures.timestamp >= time1,
database.Measures.timestamp < time2)
.order_by(asc(database.Measures.timestamp))
.all())
if not data:
data = None
else:
if watt_euros == "kwatthours" or watt_euros == "euros":
data = tools.energy(data)
if watt_euros == "euros":
data = {"value": (tools.watt_euros(0,
'night',
data['night_rate'],
db) +
tools.watt_euros(0,
'day',
data['day_rate'],
db))}
else:
data = tools.to_dict(data)
# Store in cache
r.set(watt_euros + "_" + str(sensor) + "_" + "by_id" + "_" +
str(time1) + "_" + str(time2),
json.dumps(data),
int(time2) - int(time1))
return data
def do_cache_group_timestamp(sensor, watt_euros, time1, time2, step, db,
force_refresh=True):
"""
Computes the cache (if needed) for the API call
/api/<sensor:int>/get/<watt_euros:re:watts|kwatthours|euros>/by_time/<time1:float>/<time2:float>/<step:float>
Returns the stored (or computed) data.
"""
r = redis.Redis(decode_responses=True)
if not force_refresh:
data = r.get(watt_euros + "_" + str(sensor) + "_" + "by_time" + "_" +
str(time1) + "_" + str(time2) + "_" + str(step))
if data:
# If found in cache, return it
return json.loads(data)
steps = [i for i in numpy.arange(time1, time2, step)]
steps.append(time2)
data = (db.query(database.Measures)
.filter(database.Measures.sensor_id == sensor,
database.Measures.timestamp
.between(time1, time2))
.order_by(asc(database.Measures.timestamp))
.all())
if not data:
data = [None for i in range(len(steps) - 1)]
else:
tmp = [[] for i in range(len(steps) - 1)]
for i in data:
index = bisect.bisect_left(steps, i.timestamp)
if index > 0:
index -= 1
tmp[index].append(i)
data = []
for i in tmp:
if len(i) == 0:
data.append(None)
continue
energy = tools.energy(i)
if watt_euros == "watts":
tmp_data = {"value": energy["value"] / step * 1000 * 3600,
"day_rate": energy["day_rate"] / step * 1000 * 3600,
"night_rate": energy["night_rate"] / step * 1000 * 3600}
elif watt_euros == 'kwatthours':
tmp_data = energy
elif watt_euros == 'euros':
if energy["night_rate"] != 0:
night_rate = tools.watt_euros(0,
'night',
energy['night_rate'],
db)
else:
night_rate = 0
if energy["day_rate"] != 0:
day_rate = tools.watt_euros(0,
'day',
energy['day_rate'],
db)
else:
day_rate = 0
tmp_data = {"value": night_rate + day_rate}
data.append(tmp_data)
if len(data) == 0:
data = None
# Store in cache
if time2 < datetime.datetime.now().timestamp():
# If new measures are to come, short lifetime (basically timestep)
r.setex(watt_euros + "_" + str(sensor) + "_" + "by_time" + "_" +
str(time1) + "_" + str(time2) + "_" + str(step),
json.dumps(data),
int(step))
else:
# Else, store for a greater lifetime (basically time2 - time1)
r.setex(watt_euros + "_" + str(sensor) + "_" + "by_time" + "_" +
str(time1) + "_" + str(time2) + "_" + str(step),
json.dumps(data),
int(time2 - time1))
return data

80
CitizenWatt-Base/libcitizenwatt/config.py

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
#!/usr/bin/env python3
import crypt
import errno
import json
import os
import sys
from libcitizenwatt import tools
def make_sure_path_exists(path):
try:
os.makedirs(path)
return False
except OSError as exception:
if exception.errno != errno.EEXIST:
raise
else:
return True
class Config():
def __init__(self, base_config_path="~/.config/citizenwatt/"):
self.config_path = os.path.expanduser(base_config_path)
self.config = {}
self.load()
def as_dict(self):
return self.config
def get(self, param):
return self.config.get(param, False)
def set(self, param, value):
self.config[param] = value
def initialize(self):
self.set("max_returned_values", 500)
self.set("database_type", "postgresql+psycopg2")
self.set("username", "citizenwatt")
self.set("password", "citizenwatt")
self.set("database", "citizenwatt")
self.set("host", "localhost")
self.set("debug", False)
self.set("url_energy_providers",
"http://dev.citizenwatt.paris/providers/electricity_providers.json")
self.set("salt", crypt.mksalt())
self.set("named_fifo", "/tmp/sensor")
self.set("default_timestep", 8)
self.set("port", 8080)
self.set("autoreload", False)
self.save()
def load(self):
try:
folder_exists = make_sure_path_exists(self.config_path)
if(folder_exists and
os.path.isfile(self.config_path + "config.json")):
initialized = True
else:
initialized = False
except OSError:
tools.warning("Unable to create ~/.config folder.")
sys.exit(1)
if not initialized:
self.initialize()
else:
try:
with open(self.config_path + "config.json", 'r') as fh:
self.config = json.load(fh)
except (ValueError, IOError):
tools.warning("Config file could not be read.")
sys.exit(1)
def save(self):
try:
with open(self.config_path + "config.json", 'w') as fh:
fh.write(json.dumps(self.config))
except IOError:
tools.warning("Could not write config file.")
sys.exit(1)

65
CitizenWatt-Base/libcitizenwatt/database.py

@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
#!/usr/bin/env python3
from sqlalchemy import Column, Float
from sqlalchemy import ForeignKey, Integer, Text, VARCHAR
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class Sensor(Base):
__tablename__ = "sensors"
id = Column(Integer, primary_key=True)
name = Column(VARCHAR(255), unique=True)
type_id = Column(Integer,
ForeignKey("measures_types.id", ondelete="CASCADE"),
nullable=False)
measures = relationship("Measures", passive_deletes=True)
last_timer = Column(Integer)
type = relationship("MeasureType", lazy="joined")
aes_key = Column(VARCHAR(255))
base_address = Column(VARCHAR(30))
class Measures(Base):
__tablename__ = "measures"
id = Column(Integer, primary_key=True)
sensor_id = Column(Integer,
ForeignKey("sensors.id", ondelete="CASCADE"),
nullable=False)
value = Column(Float)
timestamp = Column(Integer, index=True)
night_rate = Column(Integer) # Boolean, 1 if night_rate
class Provider(Base):
__tablename__ = "providers"
id = Column(Integer, primary_key=True)
name = Column(VARCHAR(length=255), unique=True)
type_id = Column(Integer,
ForeignKey("measures_types.id", ondelete="CASCADE"),
nullable=False)
day_slope_watt_euros = Column(Float)
day_constant_watt_euros = Column(Float)
night_slope_watt_euros = Column(Float)
night_constant_watt_euros = Column(Float)
current = Column(Integer)
threshold = Column(Integer)
class MeasureType(Base):
__tablename__ = "measures_types"
id = Column(Integer, primary_key=True)
name = Column(VARCHAR(255), unique=True)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
login = Column(VARCHAR(length=255), unique=True)
password = Column(Text)
is_admin = Column(Integer)
# Stored as seconds since beginning of day
start_night_rate = Column(Integer)
end_night_rate = Column(Integer)

108
CitizenWatt-Base/libcitizenwatt/tools.py

@ -0,0 +1,108 @@ @@ -0,0 +1,108 @@
#!/usr/bin/env python3
import numpy
import os
import sys
from libcitizenwatt import database
def warning(*objs):
"""Write warnings to stderr"""
print("WARNING: ", *objs, file=sys.stderr)
def to_dict(model):
"""Returns a JSON representation of an SQLAlchemy-backed object.
Returns a timestamp for DateTime fields, to be easily JSON serializable.
TODO : Use runtime inspection API
From https://zato.io/blog/posts/converting-sqlalchemy-objects-to-json.html
"""
if isinstance(model, list):
return [to_dict(i) for i in model]
else:
dict = {}
dict['id'] = getattr(model, 'id')
for col in model._sa_class_manager.mapper.mapped_table.columns:
if str(col.type) == "TIMESTAMP":
dict[col.name] = getattr(model, col.name).timestamp()
else:
dict[col.name] = getattr(model, col.name)
return dict
def last_day(month, year):
"""Returns the last day of month <month> of year <year>."""
if month in [1, 3, 5, 7, 8, 10, 12]:
return 31
elif month == 2:
if year % 4 == 0 and (not year % 100 or year % 400):
return 29
else:
return 28
else:
return 30
def energy(powers, default_timestep=8):
"""Compute the energy associated to a list of measures (in W)
and associated timestamps (in s).
"""
energy = {'night_rate': 0, 'day_rate': 0, 'value': 0}
if len(powers) == 1:
if powers[0].night_rate == 1:
energy["night_rate"] = (powers[0].value / 1000 *
default_timestep / 3600)
else:
energy["day_rate"] = (powers[0].value / 1000 *
default_timestep / 3600)
energy['value'] = energy['day_rate'] + energy['night_rate']
else:
x = []
day_rate = []
night_rate = []
for i in powers:
x.append(i.timestamp)
if i.night_rate == 1:
night_rate.append(i.value)
day_rate.append(0)
else:
day_rate.append(i.value)
night_rate.append(0)
energy["night_rate"] = numpy.trapz(night_rate, x) / 1000 / 3600
energy["day_rate"] = numpy.trapz(day_rate, x) / 1000 / 3600
energy['value'] = energy['day_rate'] + energy['night_rate']
return energy
def watt_euros(energy_provider, tariff, consumption, db):
if energy_provider != 0:
provider = (db.query(database.Provider)
.filter_by(id=energy_provider)
.first())
else:
provider = (db.query(database.Provider)
.filter_by(current=1)
.first())
if not provider:
data = None
else:
if tariff == "night":
data = provider.night_slope_watt_euros * consumption
elif tariff == "day":
data = provider.day_slope_watt_euros * consumption
else:
data = None
return data
def update_base_address(base_address):
"""Update the address of the base stored in
~/.config/citizenwatt/base_address
"""
path = os.path.expanduser("~/.config/citizenwatt/base_address")
with open(path, "w+") as fh:
fh.write(str(base_address))

1
CitizenWatt-Base/post_update.sh

@ -0,0 +1 @@ @@ -0,0 +1 @@
#!/bin/sh

109
CitizenWatt-Base/process.py

@ -0,0 +1,109 @@ @@ -0,0 +1,109 @@
#!/usr/bin/env python3
import datetime
import json
import os
import stat
import struct
import sys
import time
from libcitizenwatt import database
from libcitizenwatt import tools
from Crypto.Cipher import AES
from libcitizenwatt.config import Config
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
def get_rate_type(db):
"""Returns "day" or "night" according to current time
"""
user = db.query(database.User).filter_by(is_admin=1).first()
now = datetime.datetime.now()
now = 3600 * now.hour + 60 * now.minute
if user is None:
return -1
elif user.end_night_rate > user.start_night_rate:
if now > user.start_night_rate and now < user.end_night_rate:
return 1
else:
return 0
else:
if now > user.start_night_rate or now < user.end_night_rate:
return 1
else:
return 0
def get_cw_sensor():
"""Returns the citizenwatt sensor object or None"""
db = create_session()
sensor = (db.query(database.Sensor)
.filter_by(name="CitizenWatt")
.first())
db.close()
return sensor
# Configuration
config = Config()
# DB initialization
database_url = (config.get("database_type") + "://" + config.get("username") +
":" + config.get("password") + "@" + config.get("host") + "/" +
config.get("database"))
engine = create_engine(database_url, echo=config.get("debug"))
create_session = sessionmaker(bind=engine)
database.Base.metadata.create_all(engine)
sensor = get_cw_sensor()
while not sensor or not sensor.aes_key:
tools.warning("Install is not complete ! " +
"Visit http://citizenwatt.local first.")
time.sleep(1)
sensor = get_cw_sensor()
key = json.loads(sensor.aes_key)
key = struct.pack("<16B", *key)
try:
assert(stat.S_ISFIFO(os.stat(config.get("named_fifo")).st_mode))
except (AssertionError, FileNotFoundError):
sys.exit("Unable to open fifo " + config.get("named_fifo") + ".")
try:
with open(config.get("named_fifo"), 'rb') as fifo:
while True:
measure = fifo.read(16)
print("New encrypted packet:" + str(measure))
decryptor = AES.new(key, AES.MODE_ECB)
measure = decryptor.decrypt(measure)
measure = struct.unpack("<HHHLlH", measure)
print("New incoming measure:" + str(measure))
power = measure[0]
voltage = measure[1]
battery = measure[2]
timer = measure[3]
if(sensor.last_timer and sensor.last_timer > 0 and
sensor.last_timer < 4233600000 and
timer < sensor.last_timer):
tools.warning("Invalid timer in the last packet, skipping it")
else:
db = create_session()
measure_db = database.Measures(sensor_id=sensor.id,
value=power,
timestamp=datetime.datetime.now().timestamp(),
night_rate=get_rate_type(db))
db.add(measure_db)
sensor.last_timer = timer
(db.query(database.Sensor)
.filter_by(name="CitizenWatt")
.update({"last_timer": sensor.last_timer}))
db.commit()
print("Saved successfully.")
except KeyboardInterrupt:
pass

109
CitizenWatt-Base/receive.cpp

@ -0,0 +1,109 @@ @@ -0,0 +1,109 @@
#include <cstdlib>
#include <iostream>
#include <fstream>
#include <string>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <RF24.h>
volatile sig_atomic_t flag = 0;
void quit(int sig) {
flag = 1;
}
const bool DEBUG = true;
// Speed for the nrf module
// RF24_250KBPS / RF24_1MBPS / RF24_2MBPS
// Reduce it to improve reliability
const rf24_datarate_e NRF_SPEED = RF24_1MBPS;
// PreAmplifier level for the nRF
// Lower this to reduce power consumption. This will reduce range.
const rf24_pa_dbm_e NRF_PA_LEVEL = RF24_PA_LOW;
// Channel for the nrf module
// 76 is default safe channel in RF24
const int NRF_CHANNEL = 0x4c;
const uint64_t default_addr = 0xE056D446D0LL;
//RF24 radio(RPI_V2_GPIO_P1_15, RPI_V2_GPIO_P1_24, BCM2835_SPI_SPEED_8MHZ);
RF24 radio("/dev/spidev0.0",8000000 , 25);
// Named pipe
int fd;
char * myfifo = "/tmp/sensor";
int main() {
uint8_t payload[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
// Create FIFO
mkfifo(myfifo, 0666);
signal(SIGINT, quit);
// Open FIFO - while wait here until another thread opens the same fifo
fd = open(myfifo, O_WRONLY);
// Get the address to listen on
std::ifstream config_addr;
config_addr.open("~/.config/citizenwatt/base_address", std::ios::in);
uint64_t addr;
if (config_addr.is_open()) {
config_addr >> addr;
config_addr.close();
}
else {
addr = default_addr;
}
// Initialize nRF
radio.begin();
// Max number of retries and max delay between them
radio.setRetries(15, 15);
radio.setChannel(NRF_CHANNEL);
// Reduce payload size to improve reliability
radio.setPayloadSize(16);
// Set the datarate
radio.setDataRate(NRF_SPEED);
// Use the largest CRC
radio.setCRCLength(RF24_CRC_16);
// Ensure auto ACK is enabled
radio.setAutoAck(1);
// Use the best PA level
radio.setPALevel(NRF_PA_LEVEL);
// Open reading pipe
radio.openReadingPipe(1, addr);
radio.startListening();
while(1) {
if(flag) {
close(fd);
unlink(myfifo);
std::cout << "Exiting…\n";
return 0;
}
if(radio.available()) {
radio.read(&payload, sizeof(payload));
if(DEBUG) {
std::cout << "Received : ";
for(int i=0; i<sizeof(payload); i++) {
std::cout << std::hex << (int) payload[i];
}
std::cout << "\n";
}
// Send to fifo
write(fd, payload, sizeof(payload));
// Maybe needed ? fflush(fd)
}
sleep(2);
}
close(fd);
}

20
CitizenWatt-Base/startup.sh

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
#!/bin/sh
echo "Starting the webserver…"
screen -dmS visu && screen -S visu -p 0 -X stuff "while true; do python3 visu.py; done$(printf \\r)"
echo "Starting receive script…"
screen -dmS receive && screen -S receive -p 0 -X stuff "while true; do ./receive; done$(printf \\r)"
echo "Done !\n"
sleep 0.2
echo "Starting processing script…"
screen -dmS process && screen -S process -p 0 -X stuff "while true; do python3 process.py; done$(printf \\r)"
echo "Done !\n"
while ! curl -s --head http://localhost:8080 2>&1 > /dev/null; do
echo "Webserver is starting…"
sleep 1
done
echo "Webserver started !\n"
echo "Ready to start !"

425
CitizenWatt-Base/static/css/normalize.css vendored

@ -0,0 +1,425 @@ @@ -0,0 +1,425 @@
/*! normalize.css v3.0.1 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
<