bluetooth scale by nbit informatik

This commit is contained in:
Joerg Lehmann 2018-05-19 17:04:00 +02:00
parent 053bbacb01
commit f1c6115c03
15 changed files with 523 additions and 117 deletions

View File

@ -20,8 +20,6 @@ mailfrom: info@nbit.ch
mailto: joerg.lehmann@nbit.ch mailto: joerg.lehmann@nbit.ch
mailuser: nbitinf@nbit.ch mailuser: nbitinf@nbit.ch
mailpwd: ukihefak27 mailpwd: ukihefak27
balance_number: "444" balance_ussd: "*121#"
balance_command: "STATUS"
forward_sms_from_this_number: "444"
master_sms_number: "+41765006123" master_sms_number: "+41765006123"
manipulation_duration_minutes: 60 manipulation_duration_minutes: 60

View File

@ -19,6 +19,8 @@ import random
import string import string
import glob import glob
import re import re
import json
import datetime
# Root Path # Root Path
APP_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) APP_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
@ -58,6 +60,9 @@ class Scale(object):
def __init__(self, scale_config): def __init__(self, scale_config):
self.last_values = [] self.last_values = []
self.scale_config = scale_config self.scale_config = scale_config
self.accu = INVALID_VALUE
self.hum = INVALID_VALUE
self.temp = INVALID_VALUE
def __del__(self): def __del__(self):
pass pass
@ -76,6 +81,26 @@ class Scale(object):
with open(datafilename, 'a') as file: with open(datafilename, 'a') as file:
file.write('%s,%d\n' % (timestamp,weigh_in_gram)) file.write('%s,%d\n' % (timestamp,weigh_in_gram))
if self.accu != INVALID_VALUE and not(swarm_alarm):
prefix = 'accu'
datafilename = "%s/data/%s-%s-%s%s%s.log" % (APP_ROOT,prefix,self.scale_config['scale_uuid'],year,month,day)
with open(datafilename, 'a') as file:
file.write('%s,%.1f\n' % (timestamp,self.accu / 100.0))
if self.hum != INVALID_VALUE and not(swarm_alarm):
prefix = 'humidity'
datafilename = "%s/data/%s-%s-%s%s%s.log" % (APP_ROOT,prefix,self.scale_config['scale_uuid'],year,month,day)
with open(datafilename, 'a') as file:
file.write('%s,%d\n' % (timestamp,self.hum / 10))
if self.temp != INVALID_VALUE and not(swarm_alarm):
prefix = 'temp'
datafilename = "%s/data/%s-%s-%s%s%s.log" % (APP_ROOT,prefix,self.scale_config['scale_uuid'],year,month,day)
with open(datafilename, 'a') as file:
file.write('%s,%d\n' % (timestamp,self.temp / 10))
def AppendReading(self,weigh_in_gram): def AppendReading(self,weigh_in_gram):
self.last_values.append(weigh_in_gram) self.last_values.append(weigh_in_gram)
if len(self.last_values) > config_data['number_of_samples']: if len(self.last_values) > config_data['number_of_samples']:
@ -129,6 +154,41 @@ class ScaleUSB_PCE(Scale):
if res != INVALID_VALUE: if res != INVALID_VALUE:
self.AppendReading(res) self.AppendReading(res)
class ScaleBT(Scale):
def __init__(self,scale_config):
Scale.__init__(self, scale_config)
def Read(self):
res = INVALID_VALUE
filename = "/home/beieli/bt-readings/%s.json" % (self.scale_config['mac'])
try:
data = json.load(open(filename))
except:
return
# Wenn die Daten aelter als 15 Minuten sind loeschen wir das File
delta_in_seconds = (datetime.datetime.now() - datetime.datetime.strptime(data['datetime'],'%d.%m.%Y %H:%M')).total_seconds()
print(delta_in_seconds)
if ( delta_in_seconds > 15*60):
print('DEBUG [%s,%s]' % (datetime.datetime.now(),data['datetime']))
print('Readings are older than 15 minutes, removing them [%s]' % (self.scale_config['mac']))
os.remove(filename)
else:
res = ((data['w1'] - self.scale_config['offset1']) / self.scale_config['ratio1']) + ((data['w2'] - self.scale_config['offset2']) / self.scale_config['ratio2'])
if res < 0:
print('DEBUG: Read Bluetooth Scale; Value less than 0, set it to 0: %d' % (res))
res = 0
self.accu = data['accu']
self.hum = data['hum']
self.temp = data['temp']
print('DEBUG: Read Bluetooth Scale: %d' % (res))
if res != INVALID_VALUE:
self.AppendReading(res)
class ScaleDummy(Scale): class ScaleDummy(Scale):
def __init__(self,scale_config): def __init__(self,scale_config):
@ -153,6 +213,8 @@ class ScaleDummy(Scale):
if (last_value + delta < 0): if (last_value + delta < 0):
delta = random.randint(0,4) delta = random.randint(0,4)
print('DEBUG: Read Dummy Scale: %d' % (last_value + delta))
self.AppendReading(last_value + delta) self.AppendReading(last_value + delta)
@ -177,6 +239,9 @@ def main():
timeout=20) timeout=20)
scale=ScaleUSB_PCE(ser, scale_config) scale=ScaleUSB_PCE(ser, scale_config)
scales.append(scale) scales.append(scale)
elif scale_config['interface_type'] == 'btscale':
scale=ScaleBT(scale_config)
scales.append(scale)
elif scale_config['interface_type'] == 'dummy': elif scale_config['interface_type'] == 'dummy':
scale=ScaleDummy(scale_config) scale=ScaleDummy(scale_config)
scales.append(scale) scales.append(scale)

43
bin/btmon.py Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# vim: expandtab sw=4 ts=4 sts=4:
#
# Bluetooth Scale Monitor
#
# Author: Joerg Lehmann, nbit Informatik GmbH
#
"""Bluetooth Scale Monitor"""
import json
import datetime
from bluepy.btle import Scanner, DefaultDelegate
LONG = 2147483648L
INT = 32768
class ScanDelegate(DefaultDelegate):
def __init__(self):
DefaultDelegate.__init__(self)
def ProcessReading(self, mac, value):
data = {}
data['datetime'] = datetime.datetime.now().strftime("%d.%m.%Y %H:%M")
data['w1'] = long(value[0:8],16) - LONG
data['w2'] = long(value[8:16],16) - LONG
data['temp'] = int(value[16:20],16) - INT
data['hum'] = int(value[20:24],16)
data['accu'] = int(value[24:28],16)
filename = "/home/beieli/bt-readings/%s.json" % (mac)
with open(filename,'w') as outfile:
json.dump(data, outfile, indent=4, sort_keys=True)
def handleDiscovery(self, dev, isNewDev, isNewData):
if isNewData:
for (adtype, desc, value) in dev.getScanData():
if desc == 'Manufacturer' and value.startswith('1234' ):
self.ProcessReading(dev.addr, value[4:])
scanner = Scanner().withDelegate(ScanDelegate())
while True:
scanner.scan(5)

View File

@ -20,6 +20,7 @@ import glob
import shutil import shutil
import random import random
import string import string
import subprocess
from os.path import basename from os.path import basename
from email.mime.application import MIMEApplication from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
@ -273,8 +274,11 @@ def is_muted():
return res return res
def balance(): def balance(phonenumber):
send_sms([config_data['balance_number']],config_data['balance_command']) sm.Terminate()
sms_message = subprocess.check_output(["/usr/bin/sudo","/home/beieli/root-bin/get_balance.sh", config_data['balance_ussd']])
sm.Init()
send_sms([phonenumber],sms_message)
def reboot(): def reboot():
os.system('/usr/bin/sudo /sbin/init 6') os.system('/usr/bin/sudo /sbin/init 6')
@ -293,12 +297,6 @@ def command_not_understood(phonenumber, message):
def process_received_sms(phonenumber,message): def process_received_sms(phonenumber,message):
# Falls es von forward_sms_from_this_number kommt, machen wir ein Foward an
# die Master Nummer
if phonenumber == config_data['forward_sms_from_this_number']:
send_sms([config_data['master_sms_number']],message)
else:
# message in Grossbuchstaben (damit Gross-/Kleinschreibung keine Rolle spielt) # message in Grossbuchstaben (damit Gross-/Kleinschreibung keine Rolle spielt)
message_uc = message.upper() message_uc = message.upper()
@ -333,7 +331,7 @@ def process_received_sms(phonenumber,message):
elif 'MANIPULATION' in message_uc: elif 'MANIPULATION' in message_uc:
start_manipulation() start_manipulation()
elif 'BALANCE' in message_uc: elif 'BALANCE' in message_uc:
balance() balance(phonenumber)
elif 'REBOOT' in message_uc: elif 'REBOOT' in message_uc:
reboot() reboot()
elif 'SHUTDOWN' in message_uc: elif 'SHUTDOWN' in message_uc:

View File

@ -1 +0,0 @@
@reboot root /home/beieli/root-bin/gsm_poweron.py

View File

@ -1,8 +1,14 @@
[gammu] [gammu]
# Please configure this! device = /dev/ttyUSB0
port = /dev/ttyAMA0 name = Phone on USB serial port HUAWEI HUAWEI_Mobile
connection = at
[gammu1]
device = /dev/ttyUSB1
name = Phone on USB serial port HUAWEI HUAWEI_Mobile
connection = at
[gammu2]
device = /dev/ttyUSB2
name = Phone on USB serial port HUAWEI HUAWEI_Mobile
connection = at connection = at
# Debugging
#logformat = textall
logformat = errorsdate
#pin = 8296

View File

@ -1,8 +1,9 @@
#imis/internet is the apn for idea connection #imis/internet is the apn for idea connection
connect "/usr/sbin/chat -v -f /etc/chatscripts/gprs -T gprs.swisscom.ch" #connect "/usr/sbin/chat -v -f /etc/chatscripts/gprs -T gprs.swisscom.ch"
connect "/usr/sbin/chat -v -f /etc/chatscripts/gprs -T internet"
# For Raspberry Pi3 use /dev/ttyS0 as the communication port: # For Raspberry Pi3 use /dev/ttyS0 as the communication port:
/dev/ttyAMA0 /dev/ttyUSB0
# Baudrate # Baudrate
115200 115200

View File

@ -1 +1 @@
beieli ALL=(ALL) NOPASSWD: /sbin/init,/home/beieli/root-bin/connect_to_internet,/home/beieli/root-bin/disconnect_from_internet,/home/beieli/root-bin/hotspot_on,/home/beieli/root-bin/hotspot_off,/home/beieli/root-bin/sync_time_with_internet beieli ALL=(ALL) NOPASSWD: /sbin/init,/home/beieli/root-bin/connect_to_internet,/home/beieli/root-bin/disconnect_from_internet,/home/beieli/root-bin/hotspot_on,/home/beieli/root-bin/hotspot_off,/home/beieli/root-bin/sync_time_with_internet,/usr/bin/hcitool,/home/beieli/root-bin/get_balance.sh

View File

@ -0,0 +1,15 @@
[Unit]
Description=btmon service
After=syslog.target
After=network.target
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/home/beieli/bin
ExecStart=/home/beieli/bin/btmon.py
Restart=always
[Install]
WantedBy=multi-user.target

2
root-bin/get_balance.sh Executable file
View File

@ -0,0 +1,2 @@
# Erster Parameter ist der USSD Code, also z.B. *121#
/home/beieli/root-bin/ussd.py $1 | grep -v '^python-gammu' |sed 's/@//'

View File

@ -87,6 +87,33 @@ def CreateDatafile(scale_uuid,infotime):
with open(ifile, 'r') as ifile: with open(ifile, 'r') as ifile:
for line in ifile: for line in ifile:
file.write(line) file.write(line)
files = glob.glob("%s/data/temp-%s-%s.log" % (APP_ROOT,scale_uuid,my_pattern))
filename = "%s/temp-%s-%s.csv" % (mycsvdir,scale_uuid,infotime)
with open(filename, 'w') as file:
for ifile in sorted(files):
if infotime in ifile:
with_data = True
with open(ifile, 'r') as ifile:
for line in ifile:
file.write(line)
files = glob.glob("%s/data/humidity-%s-%s.log" % (APP_ROOT,scale_uuid,my_pattern))
filename = "%s/humidity-%s-%s.csv" % (mycsvdir,scale_uuid,infotime)
with open(filename, 'w') as file:
for ifile in sorted(files):
if infotime in ifile:
with_data = True
with open(ifile, 'r') as ifile:
for line in ifile:
file.write(line)
files = glob.glob("%s/data/accu-%s-%s.log" % (APP_ROOT,scale_uuid,my_pattern))
filename = "%s/accu-%s-%s.csv" % (mycsvdir,scale_uuid,infotime)
with open(filename, 'w') as file:
for ifile in sorted(files):
if infotime in ifile:
with_data = True
with open(ifile, 'r') as ifile:
for line in ifile:
file.write(line)
def GetInfoText(): def GetInfoText():
with open('%s/bin/beielimon-config.yaml' % (APP_ROOT), 'r') as f: with open('%s/bin/beielimon-config.yaml' % (APP_ROOT), 'r') as f:
@ -176,15 +203,18 @@ def server_static(filepath):
def scale_data(scale,infotime): def scale_data(scale,infotime):
scale_uuid = '' scale_uuid = ''
scale_alias = '' scale_alias = ''
interface_type = ''
for s in config_data['scales']: for s in config_data['scales']:
if s['alias'].replace(' ','_') == scale: if s['alias'].replace(' ','_') == scale:
scale_uuid = s['scale_uuid'] scale_uuid = s['scale_uuid']
scale_alias = s['alias'] scale_alias = s['alias']
interface_type = s['interface_type']
CreateDatafile(scale_uuid, infotime) CreateDatafile(scale_uuid, infotime)
data = { data = {
'scale_uuid': scale_uuid, 'scale_uuid': scale_uuid,
'scale_alias': scale_alias, 'scale_alias': scale_alias,
'interface_type': interface_type,
'infotime': infotime 'infotime': infotime
} }

127
root-bin/ussd.py Executable file
View File

@ -0,0 +1,127 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# vim: expandtab sw=4 ts=4 sts=4:
#
# Copyright © 2003 - 2017 Michal Čihař <michal@cihar.com>
#
# This file is part of python-gammu <https://wammu.eu/python-gammu/>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
'''
Service numbers dialogue example.
'''
from __future__ import print_function
import gammu
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
import time
REPLY = False
gsm = (u"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1bÆæßÉ !\"%&'()*+,-./0123456789:;<=>"
u"?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà")
def gsm7bitdecode(f):
f = ''.join(["{0:08b}".format(int(f[i:i+2], 16)) for i in range(0, len(f), 2)][::-1])
return ''.join([gsm[int(f[::-1][i:i+7][::-1], 2)] for i in range(0, len(f), 7)])
def gsm7bitencode(src):
result, count, last = [], 0, 0
for c in src:
this = ord(c) << (8 - count)
if count:
result.append('%02X' % ((last >> 8) | (this & 0xFF)))
count = (count + 1) % 8
last = this
result.append('%02x' % (last >> 8))
return ''.join(result)
def callback(state_machine, callback_type, data):
'''
Callback on USSD data.
'''
global REPLY
if callback_type != 'USSD':
print('Unexpected event type: {0}'.format(callback_type))
sys.exit(1)
REPLY = True
#print('Network reply:')
#print('Status: {0}'.format(data['Status']))
#print(data['Text'])
print(gsm7bitdecode(data['Text']))
if data['Status'] == 'ActionNeeded':
do_service(state_machine)
def init():
'''
Intializes gammu and callbacks.
'''
state_machine = gammu.StateMachine()
state_machine.ReadConfig()
state_machine.Init()
state_machine.SetIncomingCallback(callback)
try:
state_machine.SetIncomingUSSD()
except gammu.ERR_NOTSUPPORTED:
print('Incoming USSD notification is not supported.')
sys.exit(1)
return state_machine
def do_service(state_machine):
'''
Main code to talk with worker.
'''
global REPLY
if len(sys.argv) >= 2:
code = sys.argv[1]
del sys.argv[1]
else:
prompt = 'Enter code (empty string to end): '
try:
code = raw_input(prompt)
except NameError:
code = input(prompt)
code=gsm7bitencode(code)
#print(code)
if code != '':
#print('Talking to network...')
REPLY = False
state_machine.DialService(code)
loops = 0
while not REPLY and loops < 10:
#print("Loop")
state_machine.ReadDevice()
time.sleep(1)
loops += 1
def main():
state_machine = init()
do_service(state_machine)
state_machine.Terminate()
if __name__ == '__main__':
main()

View File

@ -9,44 +9,46 @@
<link rel="icon" href="/static/images/favicon.ico"> <link rel="icon" href="/static/images/favicon.ico">
<title>BeieliPi - Messkurve f&uuml;r {{ scale_alias }}</title> <title>BeieliPi - Messkurve f&uuml;r {{ scale_alias }}</title>
<link href="/static/dist/toolkit.min.css" rel="stylesheet">
<link href="/static/css/customize.css" rel="stylesheet">
<script type="text/javascript" src="/static/js/dygraph.js"></script> <script type="text/javascript" src="/static/js/dygraph.js"></script>
<script type="text/javascript" src="/static/js/smooth-plotter.js"></script>
<link rel="stylesheet" src="/static/css/dygraph.css" /> <link rel="stylesheet" src="/static/css/dygraph.css" />
<style type="text/css"> <style type="text/css">
.dygraph-axis-label-x { .dygraph-axis-label-x {
font-size: 10px; font-size: 10px;
} }
* {
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
}
#mylabel { #mylabel {
text-align: right;
font-size: 12px; font-size: 12px;
} }
#graphdiv { * {
position: absolute; font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
left: 1%;
right: 1%;
top: 1%;
bottom: 1%;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="mylabel"></div> <div class="container">
<div class="text-center"><h1>{{ scale_alias }}</h1></div>
<div id="graphdiv"></div> <div id="graphdiv"></div>
% if interface_type == 'btscale':
<div id="graphdivtemp"></div>
<div id="graphdivhum"></div>
<div id="graphdivaccu"></div>
% end
</div>
<script type="text/javascript"> <script type="text/javascript">
g = new Dygraph( g = new Dygraph(
document.getElementById("graphdiv"), document.getElementById("graphdiv"),
"/data/{{ scale_uuid }}-{{ infotime }}.csv", "/data/{{ scale_uuid }}-{{ infotime }}.csv",
{ {
title: '<b>{{ scale_alias }}</b>', title: '<b>Gewicht</b><div id="mylabel"></div>',
titleHeight: 36, titleHeight: 36,
showRangeSelector: true, showRangeSelector: true,
rangeSelectorHeight: 40, rangeSelectorHeight: 40,
@ -54,6 +56,7 @@
pointSize: 2, pointSize: 2,
labels: [ 'Timestamp', 'Gewicht in Gramm' ], labels: [ 'Timestamp', 'Gewicht in Gramm' ],
labelsDiv: document.getElementById("mylabel"), labelsDiv: document.getElementById("mylabel"),
plotter: smoothPlotter,
axes: { axes: {
x: { x: {
pixelsPerLabel: 90, pixelsPerLabel: 90,
@ -68,6 +71,91 @@
} }
} }
); );
gt = new Dygraph(
document.getElementById("graphdivtemp"),
"/data/temp-{{ scale_uuid }}-{{ infotime }}.csv",
{
title: '<b>Temparatur</b><div id="mylabel"></div>',
titleHeight: 36,
showRangeSelector: true,
rangeSelectorHeight: 40,
drawPoints: true,
pointSize: 2,
labels: [ 'Timestamp', 'Temparatur in Grad Celsius' ],
labelsDiv: document.getElementById("mylabel"),
plotter: smoothPlotter,
axes: {
x: {
pixelsPerLabel: 90,
axisLabelWidth: 90,
axisLabelFormatter: function(d, gran) {
return new Date(d).toLocaleString('de-CH').slice(0, -3);
},
valueFormatter: function(ms) {
return new Date(ms).toLocaleString('de-CH').slice(0, -3);
}
}
}
}
);
gh = new Dygraph(
document.getElementById("graphdivhum"),
"/data/humidity-{{ scale_uuid }}-{{ infotime }}.csv",
{
title: '<b>Luftfeuchtigkeit</b><div id="mylabel"></div>',
titleHeight: 36,
showRangeSelector: true,
rangeSelectorHeight: 40,
drawPoints: true,
pointSize: 2,
labels: [ 'Timestamp', 'Luftfeuchtigkeit in Prozent' ],
labelsDiv: document.getElementById("mylabel"),
plotter: smoothPlotter,
axes: {
x: {
pixelsPerLabel: 90,
axisLabelWidth: 90,
axisLabelFormatter: function(d, gran) {
return new Date(d).toLocaleString('de-CH').slice(0, -3);
},
valueFormatter: function(ms) {
return new Date(ms).toLocaleString('de-CH').slice(0, -3);
}
}
}
}
);
ga = new Dygraph(
document.getElementById("graphdivaccu"),
"/data/accu-{{ scale_uuid }}-{{ infotime }}.csv",
{
title: '<b>Batteriespannung</b><div id="mylabel"></div>',
titleHeight: 36,
showRangeSelector: true,
rangeSelectorHeight: 40,
drawPoints: true,
pointSize: 2,
labels: [ 'Timestamp', 'Batteriespannung in Volt' ],
labelsDiv: document.getElementById("mylabel"),
plotter: smoothPlotter,
axes: {
x: {
pixelsPerLabel: 90,
axisLabelWidth: 90,
axisLabelFormatter: function(d, gran) {
return new Date(d).toLocaleString('de-CH').slice(0, -3);
},
valueFormatter: function(ms) {
return new Date(ms).toLocaleString('de-CH').slice(0, -3);
}
}
}
}
);
</script> </script>
</body> </body>

View File

@ -47,9 +47,10 @@ hr {
text-align: center; text-align: center;
} }
#graphdiv { #graphdiv, #graphdivtemp, #graphdivhum, #graphdivaccu {
width: 100% !important; width: 100% !important;
min-height: 400px; min-height: 400px;
margin: 20px 20px 20px 20px;
} }

View File

@ -169,6 +169,10 @@ process.off = noop;
process.removeListener = noop; process.removeListener = noop;
process.removeAllListeners = noop; process.removeAllListeners = noop;
process.emit = noop; process.emit = noop;
process.prependListener = noop;
process.prependOnceListener = noop;
process.listeners = function (name) { return [] }
process.binding = function (name) { process.binding = function (name) {
throw new Error('process.binding is not supported'); throw new Error('process.binding is not supported');
@ -2837,7 +2841,8 @@ DygraphInteraction.defaultModel = {
// Give plugins a chance to grab this event. // Give plugins a chance to grab this event.
var e = { var e = {
canvasx: context.dragEndX, canvasx: context.dragEndX,
canvasy: context.dragEndY canvasy: context.dragEndY,
cancelable: true
}; };
if (g.cascadeEvents_('dblclick', e)) { if (g.cascadeEvents_('dblclick', e)) {
return; return;
@ -3495,6 +3500,12 @@ if (typeof process !== 'undefined') {
"type": "integer", "type": "integer",
"description": "Width, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored." "description": "Width, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."
}, },
"pixelRatio": {
"default": "(devicePixelRatio / context.backingStoreRatio)",
"labels": ["Overall display"],
"type": "float",
"description": "Overrides the pixel ratio scaling factor for the canvas's 2d context. Ordinarily, this is set to the devicePixelRatio / (context.backingStoreRatio || 1), so on mobile devices, where the devicePixelRatio can be somewhere around 3, performance can be improved by overriding this value to something less precise, like 1, at the expense of resolution."
},
"interactionModel": { "interactionModel": {
"default": "...", "default": "...",
"labels": ["Interactive Elements"], "labels": ["Interactive Elements"],
@ -3764,7 +3775,7 @@ if (typeof process !== 'undefined') {
"default": "null", "default": "null",
"labels": ["Axis display", "Interactive Elements"], "labels": ["Axis display", "Interactive Elements"],
"type": "float", "type": "float",
"description": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% pased the edges of the displayed values. null means no bounds." "description": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% passed the edges of the displayed values. null means no bounds."
}, },
"title": { "title": {
"labels": ["Chart labels"], "labels": ["Chart labels"],
@ -4694,29 +4705,36 @@ var dateTicker = function dateTicker(a, b, pixels, opts, dygraph, vals) {
exports.dateTicker = dateTicker; exports.dateTicker = dateTicker;
// Time granularity enumeration // Time granularity enumeration
var Granularity = { var Granularity = {
SECONDLY: 0, MILLISECONDLY: 0,
TWO_SECONDLY: 1, TWO_MILLISECONDLY: 1,
FIVE_SECONDLY: 2, FIVE_MILLISECONDLY: 2,
TEN_SECONDLY: 3, TEN_MILLISECONDLY: 3,
THIRTY_SECONDLY: 4, FIFTY_MILLISECONDLY: 4,
MINUTELY: 5, HUNDRED_MILLISECONDLY: 5,
TWO_MINUTELY: 6, FIVE_HUNDRED_MILLISECONDLY: 6,
FIVE_MINUTELY: 7, SECONDLY: 7,
TEN_MINUTELY: 8, TWO_SECONDLY: 8,
THIRTY_MINUTELY: 9, FIVE_SECONDLY: 9,
HOURLY: 10, TEN_SECONDLY: 10,
TWO_HOURLY: 11, THIRTY_SECONDLY: 11,
SIX_HOURLY: 12, MINUTELY: 12,
DAILY: 13, TWO_MINUTELY: 13,
TWO_DAILY: 14, FIVE_MINUTELY: 14,
WEEKLY: 15, TEN_MINUTELY: 15,
MONTHLY: 16, THIRTY_MINUTELY: 16,
QUARTERLY: 17, HOURLY: 17,
BIANNUAL: 18, TWO_HOURLY: 18,
ANNUAL: 19, SIX_HOURLY: 19,
DECADAL: 20, DAILY: 20,
CENTENNIAL: 21, TWO_DAILY: 21,
NUM_GRANULARITIES: 22 WEEKLY: 22,
MONTHLY: 23,
QUARTERLY: 24,
BIANNUAL: 25,
ANNUAL: 26,
DECADAL: 27,
CENTENNIAL: 28,
NUM_GRANULARITIES: 29
}; };
exports.Granularity = Granularity; exports.Granularity = Granularity;
@ -4747,6 +4765,13 @@ var DateField = {
* @type {Array.<{datefield:number, step:number, spacing:number}>} * @type {Array.<{datefield:number, step:number, spacing:number}>}
*/ */
var TICK_PLACEMENT = []; var TICK_PLACEMENT = [];
TICK_PLACEMENT[Granularity.MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 1, spacing: 1 };
TICK_PLACEMENT[Granularity.TWO_MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 2, spacing: 2 };
TICK_PLACEMENT[Granularity.FIVE_MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 5, spacing: 5 };
TICK_PLACEMENT[Granularity.TEN_MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 10, spacing: 10 };
TICK_PLACEMENT[Granularity.FIFTY_MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 50, spacing: 50 };
TICK_PLACEMENT[Granularity.HUNDRED_MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 100, spacing: 100 };
TICK_PLACEMENT[Granularity.FIVE_HUNDRED_MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 500, spacing: 500 };
TICK_PLACEMENT[Granularity.SECONDLY] = { datefield: DateField.DATEFIELD_SS, step: 1, spacing: 1000 * 1 }; TICK_PLACEMENT[Granularity.SECONDLY] = { datefield: DateField.DATEFIELD_SS, step: 1, spacing: 1000 * 1 };
TICK_PLACEMENT[Granularity.TWO_SECONDLY] = { datefield: DateField.DATEFIELD_SS, step: 2, spacing: 1000 * 2 }; TICK_PLACEMENT[Granularity.TWO_SECONDLY] = { datefield: DateField.DATEFIELD_SS, step: 2, spacing: 1000 * 2 };
TICK_PLACEMENT[Granularity.FIVE_SECONDLY] = { datefield: DateField.DATEFIELD_SS, step: 5, spacing: 1000 * 5 }; TICK_PLACEMENT[Granularity.FIVE_SECONDLY] = { datefield: DateField.DATEFIELD_SS, step: 5, spacing: 1000 * 5 };
@ -5011,7 +5036,7 @@ var logRangeFraction = function logRangeFraction(r0, r1, pct) {
// Original calcuation: // Original calcuation:
// pct = (log(x) - log(xRange[0])) / (log(xRange[1]) - log(xRange[0]))); // pct = (log(x) - log(xRange[0])) / (log(xRange[1]) - log(xRange[0])));
// //
// Multiply both sides by the right-side demoninator. // Multiply both sides by the right-side denominator.
// pct * (log(xRange[1] - log(xRange[0]))) = log(x) - log(xRange[0]) // pct * (log(xRange[1] - log(xRange[0]))) = log(x) - log(xRange[0])
// //
// add log(xRange[0]) to both sides // add log(xRange[0]) to both sides
@ -5279,7 +5304,7 @@ function isValidPoint(p, opt_allowNaNY) {
; ;
/** /**
* Number formatting function which mimicks the behavior of %g in printf, i.e. * Number formatting function which mimics the behavior of %g in printf, i.e.
* either exponential or fixed format (without trailing 0s) is used depending on * either exponential or fixed format (without trailing 0s) is used depending on
* the length of the generated string. The advantage of this format is that * the length of the generated string. The advantage of this format is that
* there is a predictable upper bound on the resulting string length, * there is a predictable upper bound on the resulting string length,
@ -5433,7 +5458,7 @@ function hmsString_(hh, mm, ss, ms) {
/** /**
* Convert a JS date (millis since epoch) to a formatted string. * Convert a JS date (millis since epoch) to a formatted string.
* @param {number} time The JavaScript time value (ms since epoch) * @param {number} time The JavaScript time value (ms since epoch)
* @param {boolean} utc Wether output UTC or local time * @param {boolean} utc Whether output UTC or local time
* @return {string} A date of one of these forms: * @return {string} A date of one of these forms:
* "YYYY/MM/DD", "YYYY/MM/DD HH:MM" or "YYYY/MM/DD HH:MM:SS" * "YYYY/MM/DD", "YYYY/MM/DD HH:MM" or "YYYY/MM/DD HH:MM:SS"
* @private * @private
@ -6341,6 +6366,12 @@ function dateAxisLabelFormatter(date, granularity, opts) {
if (frac === 0 || granularity >= DygraphTickers.Granularity.DAILY) { if (frac === 0 || granularity >= DygraphTickers.Granularity.DAILY) {
// e.g. '21 Jan' (%d%b) // e.g. '21 Jan' (%d%b)
return zeropad(day) + '&#160;' + SHORT_MONTH_NAMES_[month]; return zeropad(day) + '&#160;' + SHORT_MONTH_NAMES_[month];
} else if (granularity < DygraphTickers.Granularity.SECONDLY) {
// e.g. 40.310 (meaning 40 seconds and 310 milliseconds)
var str = "" + millis;
return zeropad(secs) + "." + ('000' + str).substring(str.length);
} else if (granularity > DygraphTickers.Granularity.MINUTELY) {
return hmsString_(hours, mins, secs, 0);
} else { } else {
return hmsString_(hours, mins, secs, millis); return hmsString_(hours, mins, secs, millis);
} }
@ -6420,7 +6451,7 @@ function dateValueFormatter(d, opts) {
* @param {Object} attrs Various other attributes, e.g. errorBars determines * @param {Object} attrs Various other attributes, e.g. errorBars determines
* whether the input data contains error ranges. For a complete list of * whether the input data contains error ranges. For a complete list of
* options, see http://dygraphs.com/options.html. * options, see http://dygraphs.com/options.html.
*/var Dygraph=function Dygraph(div,data,opts){this.__init__(div,data,opts);};Dygraph.NAME = "Dygraph";Dygraph.VERSION = "2.0.0"; // Various default values */var Dygraph=function Dygraph(div,data,opts){this.__init__(div,data,opts);};Dygraph.NAME = "Dygraph";Dygraph.VERSION = "2.1.0"; // Various default values
Dygraph.DEFAULT_ROLL_PERIOD = 1;Dygraph.DEFAULT_WIDTH = 480;Dygraph.DEFAULT_HEIGHT = 320; // For max 60 Hz. animation: Dygraph.DEFAULT_ROLL_PERIOD = 1;Dygraph.DEFAULT_WIDTH = 480;Dygraph.DEFAULT_HEIGHT = 320; // For max 60 Hz. animation:
Dygraph.ANIMATION_STEPS = 12;Dygraph.ANIMATION_DURATION = 200; /** Dygraph.ANIMATION_STEPS = 12;Dygraph.ANIMATION_DURATION = 200; /**
* Standard plotters. These may be used by clients. * Standard plotters. These may be used by clients.
@ -6691,9 +6722,9 @@ var target=e.target || e.fromElement;var relatedTarget=e.relatedTarget || e.toEl
// This happens when the graph is resized. // This happens when the graph is resized.
if(!this.resizeHandler_){this.resizeHandler_ = function(e){dygraph.resize();}; // Update when the window is resized. if(!this.resizeHandler_){this.resizeHandler_ = function(e){dygraph.resize();}; // Update when the window is resized.
// TODO(danvk): drop frames depending on complexity of the chart. // TODO(danvk): drop frames depending on complexity of the chart.
this.addAndTrackEvent(window,'resize',this.resizeHandler_);}};Dygraph.prototype.resizeElements_ = function(){this.graphDiv.style.width = this.width_ + "px";this.graphDiv.style.height = this.height_ + "px";var canvasScale=utils.getContextPixelRatio(this.canvas_ctx_);this.canvas_.width = this.width_ * canvasScale;this.canvas_.height = this.height_ * canvasScale;this.canvas_.style.width = this.width_ + "px"; // for IE this.addAndTrackEvent(window,'resize',this.resizeHandler_);}};Dygraph.prototype.resizeElements_ = function(){this.graphDiv.style.width = this.width_ + "px";this.graphDiv.style.height = this.height_ + "px";var pixelRatioOption=this.getNumericOption('pixelRatio');var canvasScale=pixelRatioOption || utils.getContextPixelRatio(this.canvas_ctx_);this.canvas_.width = this.width_ * canvasScale;this.canvas_.height = this.height_ * canvasScale;this.canvas_.style.width = this.width_ + "px"; // for IE
this.canvas_.style.height = this.height_ + "px"; // for IE this.canvas_.style.height = this.height_ + "px"; // for IE
if(canvasScale !== 1){this.canvas_ctx_.scale(canvasScale,canvasScale);}var hiddenScale=utils.getContextPixelRatio(this.hidden_ctx_);this.hidden_.width = this.width_ * hiddenScale;this.hidden_.height = this.height_ * hiddenScale;this.hidden_.style.width = this.width_ + "px"; // for IE if(canvasScale !== 1){this.canvas_ctx_.scale(canvasScale,canvasScale);}var hiddenScale=pixelRatioOption || utils.getContextPixelRatio(this.hidden_ctx_);this.hidden_.width = this.width_ * hiddenScale;this.hidden_.height = this.height_ * hiddenScale;this.hidden_.style.width = this.width_ + "px"; // for IE
this.hidden_.style.height = this.height_ + "px"; // for IE this.hidden_.style.height = this.height_ + "px"; // for IE
if(hiddenScale !== 1){this.hidden_ctx_.scale(hiddenScale,hiddenScale);}}; /** if(hiddenScale !== 1){this.hidden_ctx_.scale(hiddenScale,hiddenScale);}}; /**
* Detach DOM elements in the dygraph and null out all data references. * Detach DOM elements in the dygraph and null out all data references.
@ -6849,6 +6880,7 @@ var oldValueRanges=this.yAxisRanges();var newValueRanges=[];for(var i=0;i < this
*/Dygraph.prototype.resetZoom = function(){var _this4=this;var dirtyX=this.isZoomed('x');var dirtyY=this.isZoomed('y');var dirty=dirtyX || dirtyY; // Clear any selection, since it's likely to be drawn in the wrong place. */Dygraph.prototype.resetZoom = function(){var _this4=this;var dirtyX=this.isZoomed('x');var dirtyY=this.isZoomed('y');var dirty=dirtyX || dirtyY; // Clear any selection, since it's likely to be drawn in the wrong place.
this.clearSelection();if(!dirty)return; // Calculate extremes to avoid lack of padding on reset. this.clearSelection();if(!dirty)return; // Calculate extremes to avoid lack of padding on reset.
var _xAxisExtremes=this.xAxisExtremes();var _xAxisExtremes2=_slicedToArray(_xAxisExtremes,2);var minDate=_xAxisExtremes2[0];var maxDate=_xAxisExtremes2[1];var animatedZooms=this.getBooleanOption('animatedZooms');var zoomCallback=this.getFunctionOption('zoomCallback'); // TODO(danvk): merge this block w/ the code below. var _xAxisExtremes=this.xAxisExtremes();var _xAxisExtremes2=_slicedToArray(_xAxisExtremes,2);var minDate=_xAxisExtremes2[0];var maxDate=_xAxisExtremes2[1];var animatedZooms=this.getBooleanOption('animatedZooms');var zoomCallback=this.getFunctionOption('zoomCallback'); // TODO(danvk): merge this block w/ the code below.
// TODO(danvk): factor out a generic, public zoomTo method.
if(!animatedZooms){this.dateWindow_ = null;this.axes_.forEach(function(axis){if(axis.valueRange)delete axis.valueRange;});this.drawGraph_();if(zoomCallback){zoomCallback.call(this,minDate,maxDate,this.yAxisRanges());}return;}var oldWindow=null,newWindow=null,oldValueRanges=null,newValueRanges=null;if(dirtyX){oldWindow = this.xAxisRange();newWindow = [minDate,maxDate];}if(dirtyY){oldValueRanges = this.yAxisRanges();newValueRanges = this.yAxisExtremes();}this.doAnimatedZoom(oldWindow,newWindow,oldValueRanges,newValueRanges,function(){_this4.dateWindow_ = null;_this4.axes_.forEach(function(axis){if(axis.valueRange)delete axis.valueRange;});if(zoomCallback){zoomCallback.call(_this4,minDate,maxDate,_this4.yAxisRanges());}});}; /** if(!animatedZooms){this.dateWindow_ = null;this.axes_.forEach(function(axis){if(axis.valueRange)delete axis.valueRange;});this.drawGraph_();if(zoomCallback){zoomCallback.call(this,minDate,maxDate,this.yAxisRanges());}return;}var oldWindow=null,newWindow=null,oldValueRanges=null,newValueRanges=null;if(dirtyX){oldWindow = this.xAxisRange();newWindow = [minDate,maxDate];}if(dirtyY){oldValueRanges = this.yAxisRanges();newValueRanges = this.yAxisExtremes();}this.doAnimatedZoom(oldWindow,newWindow,oldValueRanges,newValueRanges,function(){_this4.dateWindow_ = null;_this4.axes_.forEach(function(axis){if(axis.valueRange)delete axis.valueRange;});if(zoomCallback){zoomCallback.call(_this4,minDate,maxDate,_this4.yAxisRanges());}});}; /**
* Combined animation logic for all zoom functions. * Combined animation logic for all zoom functions.
* either the x parameters or y parameters may be null. * either the x parameters or y parameters may be null.
@ -7265,7 +7297,7 @@ if('rollPeriod' in attrs){this.rollPeriod_ = attrs.rollPeriod;}if('dateWindow' i
// highlightCircleSize // highlightCircleSize
// Check if this set options will require new points. // Check if this set options will require new points.
var requiresNewPoints=utils.isPixelChangingOptionList(this.attr_("labels"),attrs);utils.updateDeep(this.user_attrs_,attrs);this.attributes_.reparseSeries();if(file){ // This event indicates that the data is about to change, but hasn't yet. var requiresNewPoints=utils.isPixelChangingOptionList(this.attr_("labels"),attrs);utils.updateDeep(this.user_attrs_,attrs);this.attributes_.reparseSeries();if(file){ // This event indicates that the data is about to change, but hasn't yet.
// TODO(danvk): support cancelation of the update via this event. // TODO(danvk): support cancellation of the update via this event.
this.cascadeEvents_('dataWillUpdate',{});this.file_ = file;if(!block_redraw)this.start_();}else {if(!block_redraw){if(requiresNewPoints){this.predraw_();}else {this.renderGraph_(false);}}}}; /** this.cascadeEvents_('dataWillUpdate',{});this.file_ = file;if(!block_redraw)this.start_();}else {if(!block_redraw){if(requiresNewPoints){this.predraw_();}else {this.renderGraph_(false);}}}}; /**
* Make a copy of input attributes, removing file as a convenience. * Make a copy of input attributes, removing file as a convenience.
* @private * @private
@ -8796,8 +8828,8 @@ rangeSelector.prototype.updateVisibility_ = function () {
* Resizes the range selector. * Resizes the range selector.
*/ */
rangeSelector.prototype.resize_ = function () { rangeSelector.prototype.resize_ = function () {
function setElementRect(canvas, context, rect) { function setElementRect(canvas, context, rect, pixelRatioOption) {
var canvasScale = utils.getContextPixelRatio(context); var canvasScale = pixelRatioOption || utils.getContextPixelRatio(context);
canvas.style.top = rect.y + 'px'; canvas.style.top = rect.y + 'px';
canvas.style.left = rect.x + 'px'; canvas.style.left = rect.x + 'px';
@ -8824,8 +8856,9 @@ rangeSelector.prototype.resize_ = function () {
h: this.getOption_('rangeSelectorHeight') h: this.getOption_('rangeSelectorHeight')
}; };
setElementRect(this.bgcanvas_, this.bgcanvas_ctx_, this.canvasRect_); var pixelRatioOption = this.dygraph_.getNumericOption('pixelRatio');
setElementRect(this.fgcanvas_, this.fgcanvas_ctx_, this.canvasRect_); setElementRect(this.bgcanvas_, this.bgcanvas_ctx_, this.canvasRect_, pixelRatioOption);
setElementRect(this.fgcanvas_, this.fgcanvas_ctx_, this.canvasRect_, pixelRatioOption);
}; };
/** /**