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
mailuser: nbitinf@nbit.ch
mailpwd: ukihefak27
balance_number: "444"
balance_command: "STATUS"
forward_sms_from_this_number: "444"
balance_ussd: "*121#"
master_sms_number: "+41765006123"
manipulation_duration_minutes: 60

View File

@ -19,6 +19,8 @@ import random
import string
import glob
import re
import json
import datetime
# Root Path
APP_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
@ -58,6 +60,9 @@ class Scale(object):
def __init__(self, scale_config):
self.last_values = []
self.scale_config = scale_config
self.accu = INVALID_VALUE
self.hum = INVALID_VALUE
self.temp = INVALID_VALUE
def __del__(self):
pass
@ -76,6 +81,26 @@ class Scale(object):
with open(datafilename, 'a') as file:
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):
self.last_values.append(weigh_in_gram)
if len(self.last_values) > config_data['number_of_samples']:
@ -129,6 +154,41 @@ class ScaleUSB_PCE(Scale):
if res != INVALID_VALUE:
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):
def __init__(self,scale_config):
@ -153,6 +213,8 @@ class ScaleDummy(Scale):
if (last_value + delta < 0):
delta = random.randint(0,4)
print('DEBUG: Read Dummy Scale: %d' % (last_value + delta))
self.AppendReading(last_value + delta)
@ -177,6 +239,9 @@ def main():
timeout=20)
scale=ScaleUSB_PCE(ser, scale_config)
scales.append(scale)
elif scale_config['interface_type'] == 'btscale':
scale=ScaleBT(scale_config)
scales.append(scale)
elif scale_config['interface_type'] == 'dummy':
scale=ScaleDummy(scale_config)
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 random
import string
import subprocess
from os.path import basename
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
@ -273,8 +274,11 @@ def is_muted():
return res
def balance():
send_sms([config_data['balance_number']],config_data['balance_command'])
def balance(phonenumber):
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():
os.system('/usr/bin/sudo /sbin/init 6')
@ -293,12 +297,6 @@ def command_not_understood(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_uc = message.upper()
@ -333,7 +331,7 @@ def process_received_sms(phonenumber,message):
elif 'MANIPULATION' in message_uc:
start_manipulation()
elif 'BALANCE' in message_uc:
balance()
balance(phonenumber)
elif 'REBOOT' in message_uc:
reboot()
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]
# Please configure this!
port = /dev/ttyAMA0
device = /dev/ttyUSB0
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
# Debugging
#logformat = textall
logformat = errorsdate
#pin = 8296

View File

@ -1,8 +1,9 @@
#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:
/dev/ttyAMA0
/dev/ttyUSB0
# Baudrate
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:
for line in ifile:
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():
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):
scale_uuid = ''
scale_alias = ''
interface_type = ''
for s in config_data['scales']:
if s['alias'].replace(' ','_') == scale:
scale_uuid = s['scale_uuid']
scale_alias = s['alias']
interface_type = s['interface_type']
CreateDatafile(scale_uuid, infotime)
data = {
'scale_uuid': scale_uuid,
'scale_alias': scale_alias,
'interface_type': interface_type,
'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">
<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/smooth-plotter.js"></script>
<link rel="stylesheet" src="/static/css/dygraph.css" />
<style type="text/css">
.dygraph-axis-label-x {
font-size: 10px;
}
* {
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
}
#mylabel {
text-align: right;
font-size: 12px;
}
#graphdiv {
position: absolute;
left: 1%;
right: 1%;
top: 1%;
bottom: 1%;
* {
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
}
</style>
</head>
<body>
<div id="mylabel"></div>
<div class="container">
<div class="text-center"><h1>{{ scale_alias }}</h1></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">
g = new Dygraph(
document.getElementById("graphdiv"),
"/data/{{ scale_uuid }}-{{ infotime }}.csv",
{
title: '<b>{{ scale_alias }}</b>',
title: '<b>Gewicht</b><div id="mylabel"></div>',
titleHeight: 36,
showRangeSelector: true,
rangeSelectorHeight: 40,
@ -54,6 +56,7 @@
pointSize: 2,
labels: [ 'Timestamp', 'Gewicht in Gramm' ],
labelsDiv: document.getElementById("mylabel"),
plotter: smoothPlotter,
axes: {
x: {
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>
</body>

View File

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

View File

@ -169,6 +169,10 @@ process.off = noop;
process.removeListener = noop;
process.removeAllListeners = noop;
process.emit = noop;
process.prependListener = noop;
process.prependOnceListener = noop;
process.listeners = function (name) { return [] }
process.binding = function (name) {
throw new Error('process.binding is not supported');
@ -2837,7 +2841,8 @@ DygraphInteraction.defaultModel = {
// Give plugins a chance to grab this event.
var e = {
canvasx: context.dragEndX,
canvasy: context.dragEndY
canvasy: context.dragEndY,
cancelable: true
};
if (g.cascadeEvents_('dblclick', e)) {
return;
@ -3495,6 +3500,12 @@ if (typeof process !== 'undefined') {
"type": "integer",
"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": {
"default": "...",
"labels": ["Interactive Elements"],
@ -3764,7 +3775,7 @@ if (typeof process !== 'undefined') {
"default": "null",
"labels": ["Axis display", "Interactive Elements"],
"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": {
"labels": ["Chart labels"],
@ -4694,29 +4705,36 @@ var dateTicker = function dateTicker(a, b, pixels, opts, dygraph, vals) {
exports.dateTicker = dateTicker;
// Time granularity enumeration
var Granularity = {
SECONDLY: 0,
TWO_SECONDLY: 1,
FIVE_SECONDLY: 2,
TEN_SECONDLY: 3,
THIRTY_SECONDLY: 4,
MINUTELY: 5,
TWO_MINUTELY: 6,
FIVE_MINUTELY: 7,
TEN_MINUTELY: 8,
THIRTY_MINUTELY: 9,
HOURLY: 10,
TWO_HOURLY: 11,
SIX_HOURLY: 12,
DAILY: 13,
TWO_DAILY: 14,
WEEKLY: 15,
MONTHLY: 16,
QUARTERLY: 17,
BIANNUAL: 18,
ANNUAL: 19,
DECADAL: 20,
CENTENNIAL: 21,
NUM_GRANULARITIES: 22
MILLISECONDLY: 0,
TWO_MILLISECONDLY: 1,
FIVE_MILLISECONDLY: 2,
TEN_MILLISECONDLY: 3,
FIFTY_MILLISECONDLY: 4,
HUNDRED_MILLISECONDLY: 5,
FIVE_HUNDRED_MILLISECONDLY: 6,
SECONDLY: 7,
TWO_SECONDLY: 8,
FIVE_SECONDLY: 9,
TEN_SECONDLY: 10,
THIRTY_SECONDLY: 11,
MINUTELY: 12,
TWO_MINUTELY: 13,
FIVE_MINUTELY: 14,
TEN_MINUTELY: 15,
THIRTY_MINUTELY: 16,
HOURLY: 17,
TWO_HOURLY: 18,
SIX_HOURLY: 19,
DAILY: 20,
TWO_DAILY: 21,
WEEKLY: 22,
MONTHLY: 23,
QUARTERLY: 24,
BIANNUAL: 25,
ANNUAL: 26,
DECADAL: 27,
CENTENNIAL: 28,
NUM_GRANULARITIES: 29
};
exports.Granularity = Granularity;
@ -4747,6 +4765,13 @@ var DateField = {
* @type {Array.<{datefield:number, step:number, spacing:number}>}
*/
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.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 };
@ -5011,7 +5036,7 @@ var logRangeFraction = function logRangeFraction(r0, r1, pct) {
// Original calcuation:
// 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])
//
// 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
* the length of the generated string. The advantage of this format is that
* 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.
* @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:
* "YYYY/MM/DD", "YYYY/MM/DD HH:MM" or "YYYY/MM/DD HH:MM:SS"
* @private
@ -6341,6 +6366,12 @@ function dateAxisLabelFormatter(date, granularity, opts) {
if (frac === 0 || granularity >= DygraphTickers.Granularity.DAILY) {
// e.g. '21 Jan' (%d%b)
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 {
return hmsString_(hours, mins, secs, millis);
}
@ -6420,7 +6451,7 @@ function dateValueFormatter(d, opts) {
* @param {Object} attrs Various other attributes, e.g. errorBars determines
* whether the input data contains error ranges. For a complete list of
* 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.ANIMATION_STEPS = 12;Dygraph.ANIMATION_DURATION = 200; /**
* 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.
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.
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
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
if(hiddenScale !== 1){this.hidden_ctx_.scale(hiddenScale,hiddenScale);}}; /**
* 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.
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.
// 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());}});}; /**
* Combined animation logic for all zoom functions.
* 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
// 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.
// 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);}}}}; /**
* Make a copy of input attributes, removing file as a convenience.
* @private
@ -8796,8 +8828,8 @@ rangeSelector.prototype.updateVisibility_ = function () {
* Resizes the range selector.
*/
rangeSelector.prototype.resize_ = function () {
function setElementRect(canvas, context, rect) {
var canvasScale = utils.getContextPixelRatio(context);
function setElementRect(canvas, context, rect, pixelRatioOption) {
var canvasScale = pixelRatioOption || utils.getContextPixelRatio(context);
canvas.style.top = rect.y + 'px';
canvas.style.left = rect.x + 'px';
@ -8824,8 +8856,9 @@ rangeSelector.prototype.resize_ = function () {
h: this.getOption_('rangeSelectorHeight')
};
setElementRect(this.bgcanvas_, this.bgcanvas_ctx_, this.canvasRect_);
setElementRect(this.fgcanvas_, this.fgcanvas_ctx_, this.canvasRect_);
var pixelRatioOption = this.dygraph_.getNumericOption('pixelRatio');
setElementRect(this.bgcanvas_, this.bgcanvas_ctx_, this.canvasRect_, pixelRatioOption);
setElementRect(this.fgcanvas_, this.fgcanvas_ctx_, this.canvasRect_, pixelRatioOption);
};
/**