diff --git a/bin/beielimon-config.yaml b/bin/beielimon-config.yaml index 3104131..2b9c693 100644 --- a/bin/beielimon-config.yaml +++ b/bin/beielimon-config.yaml @@ -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 diff --git a/bin/beielimon.py b/bin/beielimon.py index 7c3497d..38df251 100755 --- a/bin/beielimon.py +++ b/bin/beielimon.py @@ -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) diff --git a/bin/btmon.py b/bin/btmon.py new file mode 100755 index 0000000..0c6e222 --- /dev/null +++ b/bin/btmon.py @@ -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) diff --git a/bin/smsmon.py b/bin/smsmon.py index 9d970b7..19f0e75 100755 --- a/bin/smsmon.py +++ b/bin/smsmon.py @@ -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,57 +297,51 @@ 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) + # message in Grossbuchstaben (damit Gross-/Kleinschreibung keine Rolle spielt) + message_uc = message.upper() - else: - # message in Grossbuchstaben (damit Gross-/Kleinschreibung keine Rolle spielt) - message_uc = message.upper() - - # Bestimmung, ob es eine gueltige Telefonnummer ist - valid_number = False - for s in config_data['scales']: - if phonenumber in s['sms_alert_phonenumbers']: - valid_number = True + # Bestimmung, ob es eine gueltige Telefonnummer ist + valid_number = False + for s in config_data['scales']: + if phonenumber in s['sms_alert_phonenumbers']: + valid_number = True - if not valid_number: - print("Da versucht ein unberechtigter, etwas abzufragen... (Nummer: %s)" % (phonenumber)) - return + if not valid_number: + print("Da versucht ein unberechtigter, etwas abzufragen... (Nummer: %s)" % (phonenumber)) + return - if 'HELP INFO' in message_uc: - send_help(phonenumber,'info') - elif 'HELP MANIPULATION' in message_uc: - send_help(phonenumber,'manipulation') - elif 'HELP HELP' in message_uc: - send_help(phonenumber,'help') - elif 'HELP BALANCE' in message_uc: - send_help(phonenumber,'balance') - elif 'HELP REBOOT' in message_uc: - send_help(phonenumber,'reboot') - elif 'HELP SHUTDOWN' in message_uc: - send_help(phonenumber,'shutdown') - elif 'HELP HOTSPOT' in message_uc: - send_help(phonenumber,'hotspot') - elif 'HELP' in message_uc: - send_help(phonenumber,'') - elif 'INFO' in message_uc: - send_info(phonenumber, message_uc) - elif 'MANIPULATION' in message_uc: - start_manipulation() - elif 'BALANCE' in message_uc: - balance() - elif 'REBOOT' in message_uc: - reboot() - elif 'SHUTDOWN' in message_uc: - shutdown() - elif 'HOTSPOT OFF' in message_uc: - hotspot_off() - elif 'HOTSPOT ON' in message_uc: - hotspot_on() - else: - command_not_understood(phonenumber, message) + if 'HELP INFO' in message_uc: + send_help(phonenumber,'info') + elif 'HELP MANIPULATION' in message_uc: + send_help(phonenumber,'manipulation') + elif 'HELP HELP' in message_uc: + send_help(phonenumber,'help') + elif 'HELP BALANCE' in message_uc: + send_help(phonenumber,'balance') + elif 'HELP REBOOT' in message_uc: + send_help(phonenumber,'reboot') + elif 'HELP SHUTDOWN' in message_uc: + send_help(phonenumber,'shutdown') + elif 'HELP HOTSPOT' in message_uc: + send_help(phonenumber,'hotspot') + elif 'HELP' in message_uc: + send_help(phonenumber,'') + elif 'INFO' in message_uc: + send_info(phonenumber, message_uc) + elif 'MANIPULATION' in message_uc: + start_manipulation() + elif 'BALANCE' in message_uc: + balance(phonenumber) + elif 'REBOOT' in message_uc: + reboot() + elif 'SHUTDOWN' in message_uc: + shutdown() + elif 'HOTSPOT OFF' in message_uc: + hotspot_off() + elif 'HOTSPOT ON' in message_uc: + hotspot_on() + else: + command_not_understood(phonenumber, message) def main(): diff --git a/install-files/etc/cron.d/gsm_poweron b/install-files/etc/cron.d/gsm_poweron deleted file mode 100644 index 83e1d64..0000000 --- a/install-files/etc/cron.d/gsm_poweron +++ /dev/null @@ -1 +0,0 @@ -@reboot root /home/beieli/root-bin/gsm_poweron.py diff --git a/install-files/etc/gammurc b/install-files/etc/gammurc index f962880..5173242 100644 --- a/install-files/etc/gammurc +++ b/install-files/etc/gammurc @@ -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 diff --git a/install-files/etc/ppp/peers/rnet b/install-files/etc/ppp/peers/rnet index d66c74b..61eb7a8 100644 --- a/install-files/etc/ppp/peers/rnet +++ b/install-files/etc/ppp/peers/rnet @@ -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 diff --git a/install-files/etc/sudoers.d/beieli b/install-files/etc/sudoers.d/beieli index cc1422b..0866c9d 100644 --- a/install-files/etc/sudoers.d/beieli +++ b/install-files/etc/sudoers.d/beieli @@ -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 diff --git a/install-files/etc/systemd/system/btmon.service b/install-files/etc/systemd/system/btmon.service new file mode 100644 index 0000000..86d84d3 --- /dev/null +++ b/install-files/etc/systemd/system/btmon.service @@ -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 diff --git a/root-bin/get_balance.sh b/root-bin/get_balance.sh new file mode 100755 index 0000000..00abf73 --- /dev/null +++ b/root-bin/get_balance.sh @@ -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/@//' diff --git a/root-bin/my_web_server.py b/root-bin/my_web_server.py index 577ddf1..920da9c 100755 --- a/root-bin/my_web_server.py +++ b/root-bin/my_web_server.py @@ -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 } diff --git a/root-bin/ussd.py b/root-bin/ussd.py new file mode 100755 index 0000000..1273290 --- /dev/null +++ b/root-bin/ussd.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# vim: expandtab sw=4 ts=4 sts=4: +# +# Copyright © 2003 - 2017 Michal Čihař +# +# This file is part of 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() + diff --git a/web-root/scale.tpl b/web-root/scale.tpl index f5b6696..6e62622 100644 --- a/web-root/scale.tpl +++ b/web-root/scale.tpl @@ -9,44 +9,46 @@ BeieliPi - Messkurve für {{ scale_alias }} + + + + -
-
+
+

{{ scale_alias }}

+
+% if interface_type == 'btscale': +
+
+
+% end +
+ diff --git a/web-root/static/css/customize.css b/web-root/static/css/customize.css index 166bc7f..5a71c18 100644 --- a/web-root/static/css/customize.css +++ b/web-root/static/css/customize.css @@ -47,9 +47,10 @@ hr { text-align: center; } -#graphdiv { +#graphdiv, #graphdivtemp, #graphdivhum, #graphdivaccu { width: 100% !important; min-height: 400px; + margin: 20px 20px 20px 20px; } diff --git a/web-root/static/js/dygraph.js b/web-root/static/js/dygraph.js index 4790b3f..dff393a 100644 --- a/web-root/static/js/dygraph.js +++ b/web-root/static/js/dygraph.js @@ -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) + ' ' + 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); }; /**