|
|
|
@ -1,11 +1,29 @@
|
|
|
|
|
#!/usr/bin/python
|
|
|
|
|
# $Id: netatmo.py 1489 2016-04-27 09:58:13Z mwall $
|
|
|
|
|
# Copyright 2015 Matthew Wall
|
|
|
|
|
#
|
|
|
|
|
# Thanks to phillippe larduinat for publishing lnetatmo.py
|
|
|
|
|
# https://github.com/philippelt/netatmo-api-python
|
|
|
|
|
# The netatmo API has changed quite a bit since phillippe wrote the code, but
|
|
|
|
|
# the implementation helps.
|
|
|
|
|
#
|
|
|
|
|
# Shame on netatmo for making it very difficult to get data from the hardware
|
|
|
|
|
# without going through their servers.
|
|
|
|
|
#
|
|
|
|
|
# More shame on netatmo for encrypting the communication between the netatmo
|
|
|
|
|
# station and the netatmo servers, without providing end-users a way to access
|
|
|
|
|
# their data locally. They used to send data in the clear up to firmware 101,
|
|
|
|
|
# then instead of fixing a stupid decision to send the wifi network password
|
|
|
|
|
# to the netatmo servers, they chose to encrypt the communication altogether.
|
|
|
|
|
# Beware that netatmo might still be receiving your network passwords.
|
|
|
|
|
|
|
|
|
|
"""To use cloud mode you must obtain a client_id and client_secret from
|
|
|
|
|
the netatmo development web servers https://dev.netatmo.com
|
|
|
|
|
|
|
|
|
|
This driver supports multiple devices, and all known modules on each device.
|
|
|
|
|
As of april 2016 that means the base station, 'outside' T/H, additional T/H,
|
|
|
|
|
rain, and wind.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import with_statement
|
|
|
|
|
import Queue
|
|
|
|
@ -21,9 +39,19 @@ import urllib2
|
|
|
|
|
import weewx.drivers
|
|
|
|
|
import weewx.engine
|
|
|
|
|
import weewx.units
|
|
|
|
|
import weewx.wxformulas
|
|
|
|
|
|
|
|
|
|
DRIVER_NAME = 'netatmo'
|
|
|
|
|
DRIVER_VERSION = "X"
|
|
|
|
|
|
|
|
|
|
INHG_PER_MBAR = 0.0295299830714
|
|
|
|
|
CM_PER_IN = 2.54
|
|
|
|
|
MPH_TO_KPH = 1.60934
|
|
|
|
|
MPS_TO_KPH = 3.6
|
|
|
|
|
BEAFORT_TO_KPH = {0: 1, 1: 3.0, 2: 9.0, 3: 15.0, 4: 24.0, 5: 34.0, 6: 43.0,
|
|
|
|
|
7: 55.0, 8: 68.0, 9: 82.0, 10: 95.0, 11: 110.0, 12: 120.0}
|
|
|
|
|
KNOT_TO_KPH = 1.852
|
|
|
|
|
|
|
|
|
|
DRIVER_NAME = 'Netatmo'
|
|
|
|
|
DRIVER_VERSION = "0.2"
|
|
|
|
|
|
|
|
|
|
def logmsg(level, msg):
|
|
|
|
|
syslog.syslog(level, 'netatmo: %s: %s' %
|
|
|
|
@ -38,44 +66,105 @@ def loginf(msg):
|
|
|
|
|
def logerr(msg):
|
|
|
|
|
logmsg(syslog.LOG_ERR, msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def loader(config_dict, engine):
|
|
|
|
|
return NetatmoDriver(**config_dict[DRIVER_NAME])
|
|
|
|
|
|
|
|
|
|
def confeditor_loader(config_dict):
|
|
|
|
|
return NetatmoConfEditor()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NetatmoConfEditor(weewx.drivers.AbstractConfEditor):
|
|
|
|
|
@property
|
|
|
|
|
def default_stanza(self):
|
|
|
|
|
return """
|
|
|
|
|
[netatmo]
|
|
|
|
|
# This section is for the netatmo station.
|
|
|
|
|
|
|
|
|
|
# The mode specifies how driver should obtain data. The 'cloud' mode will
|
|
|
|
|
# retrieve data from the netatmo.com servers. The 'sniff' mode will parse
|
|
|
|
|
# packets from the netatmo station on the local network.
|
|
|
|
|
mode = cloud
|
|
|
|
|
|
|
|
|
|
# The cloud mode requires credentials:
|
|
|
|
|
username = INSERT_USERNAME_HERE
|
|
|
|
|
password = INSERT_PASSWORD_HERE
|
|
|
|
|
client_id = INSERT_CLIENT_ID_HERE
|
|
|
|
|
client_secret = INSERT_CLIENT_SECRET_HERE
|
|
|
|
|
|
|
|
|
|
# The driver itself
|
|
|
|
|
driver = user.netatmo
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def prompt_for_settings(self):
|
|
|
|
|
settings = dict()
|
|
|
|
|
print "Specify the mode for obtaining data, either 'cloud' or 'sniff'"
|
|
|
|
|
settings['mode'] = self._prompt('mode', 'cloud', ['cloud', 'sniff'])
|
|
|
|
|
if settings['mode'] == 'cloud':
|
|
|
|
|
print "Specify the username for netatmo.com"
|
|
|
|
|
self._prompt('username')
|
|
|
|
|
print "Specify the password for netatmo.com"
|
|
|
|
|
self._prompt('password')
|
|
|
|
|
print "Specify the client_id from dev.netatmo.com"
|
|
|
|
|
self._prompt('client_id')
|
|
|
|
|
print "Specify the client_secret from dev.netatmo.com"
|
|
|
|
|
self._prompt('client_secret')
|
|
|
|
|
return settings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NetatmoDriver(weewx.drivers.AbstractDevice):
|
|
|
|
|
DEFAULT_PORT = 80
|
|
|
|
|
DEFAULT_HOST = ''
|
|
|
|
|
# map from netatmo names to database schema names
|
|
|
|
|
DEFAULT_SENSOR_MAP = {
|
|
|
|
|
'pressure': 'pressure',
|
|
|
|
|
'temperature_in': 'inTemp',
|
|
|
|
|
'humidity_in': 'inHumidity',
|
|
|
|
|
'temperature_out': 'outTemp',
|
|
|
|
|
'humidity_out': 'outHumidity',
|
|
|
|
|
'temperature_1': 'extraTemp1',
|
|
|
|
|
'humidity_1': 'extraHumid1',
|
|
|
|
|
'temperature_2': 'extraTemp2',
|
|
|
|
|
'humidity_2': 'extraHumid2',
|
|
|
|
|
'temperature_3': 'extraTemp3',
|
|
|
|
|
'humidity_3': 'extraHumid3',
|
|
|
|
|
'wind_speed': 'windSpeed',
|
|
|
|
|
'wind_dir': 'windDir',
|
|
|
|
|
'rain': 'rain',
|
|
|
|
|
'co2': 'co2',
|
|
|
|
|
'noise': 'noise'}
|
|
|
|
|
'*.NAMain.AbsolutePressure': 'pressure',
|
|
|
|
|
'*.NAMain.Temperature': 'inTemp',
|
|
|
|
|
'*.NAMain.Humidity': 'inHumidity',
|
|
|
|
|
'*.NAMain.CO2': 'co2',
|
|
|
|
|
'*.NAMain.Noise': 'noise',
|
|
|
|
|
'*.NAMain.wifi_status': 'wifi_status',
|
|
|
|
|
'*.NAModule1.Temperature': 'outTemp',
|
|
|
|
|
'*.NAModule1.Humidity': 'outHumidity',
|
|
|
|
|
'*.NAModule1.rf_status': 'rf_status',
|
|
|
|
|
'*.NAModule1.battery_vp': 'battery_vp',
|
|
|
|
|
'*.NAModule4.Temperature': 'extraTemp1',
|
|
|
|
|
'*.NAModule4.Humidity': 'extraHumid1',
|
|
|
|
|
'*.NAModule4.rf_status': 'rf_status',
|
|
|
|
|
'*.NAModule4.battery_vp': 'battery_vp',
|
|
|
|
|
'*.NAModule2.WindStrength': 'windSpeed',
|
|
|
|
|
'*.NAModule2.WindAngle': 'windDir',
|
|
|
|
|
'*.NAModule2.GustStrength': 'windGust',
|
|
|
|
|
'*.NAModule2.GustAngle': 'windGustDir',
|
|
|
|
|
'*.NAModule2.rf_status': 'rf_status',
|
|
|
|
|
'*.NAModule2.battery_vp': 'battery_vp',
|
|
|
|
|
'*.NAModule3.Rain': 'rain',
|
|
|
|
|
'*.NAModule3.rf_status': 'rf_status',
|
|
|
|
|
'*.NAModule3.battery_vp': 'battery_vp'}
|
|
|
|
|
|
|
|
|
|
def __init__(self, **stn_dict):
|
|
|
|
|
loginf("driver version is %s" % DRIVER_VERSION)
|
|
|
|
|
self.sensor_map = stn_dict.get('sensor_map', NetatmoDriver.DEFAULT_SENSOR_MAP)
|
|
|
|
|
self.sensor_map = stn_dict.get(
|
|
|
|
|
'sensor_map', NetatmoDriver.DEFAULT_SENSOR_MAP)
|
|
|
|
|
device_id = stn_dict.get('device_id', None)
|
|
|
|
|
mode = stn_dict.get('mode', 'cloud')
|
|
|
|
|
self.max_tries = int(stn_dict.get('max_tries', 5))
|
|
|
|
|
self.retry_wait = int(stn_dict.get('retry_wait', 10)) # seconds
|
|
|
|
|
self.poll_interval = int(stn_dict.get('poll_interval', 600)) # seconds
|
|
|
|
|
timeout = int(stn_dict.get('timeout', 3))
|
|
|
|
|
port = int(stn_dict.get('port', 4200))
|
|
|
|
|
addr = stn_dict.get('host', '')
|
|
|
|
|
if mode == 'sniff':
|
|
|
|
|
self.collector = PacketSniffer(addr, port)
|
|
|
|
|
if mode.lower() == 'sniff':
|
|
|
|
|
port = int(stn_dict.get('port', NetatmoDriver.DEFAULT_PORT))
|
|
|
|
|
addr = stn_dict.get('host', NetatmoDriver.DEFAULT_HOST)
|
|
|
|
|
self.collector = PacketSniffer((addr, port))
|
|
|
|
|
elif mode.lower() == 'cloud':
|
|
|
|
|
max_tries = int(stn_dict.get('max_tries', 5))
|
|
|
|
|
retry_wait = int(stn_dict.get('retry_wait', 10)) # seconds
|
|
|
|
|
poll_interval = int(stn_dict.get('poll_interval', 600)) # seconds
|
|
|
|
|
username = stn_dict['username']
|
|
|
|
|
password = stn_dict['password']
|
|
|
|
|
client_id = stn_dict['client_id']
|
|
|
|
|
client_secret = stn_dict['client_secret']
|
|
|
|
|
self.collector = CloudClient(
|
|
|
|
|
username, password, client_id, client_secret,
|
|
|
|
|
device_id=device_id, poll_interval=poll_interval,
|
|
|
|
|
max_tries=max_tries, retry_wait=retry_wait)
|
|
|
|
|
else:
|
|
|
|
|
self.collector = CloudClient()
|
|
|
|
|
raise ValueError("unsupported mode '%s'" % mode)
|
|
|
|
|
self.collector.startup()
|
|
|
|
|
|
|
|
|
|
def closePort(self):
|
|
|
|
@ -89,20 +178,47 @@ class NetatmoDriver(weewx.drivers.AbstractDevice):
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
data = self.collector.queue.get(True, 10)
|
|
|
|
|
logdbg('data: %s' % data)
|
|
|
|
|
pkt = self.data_to_packet(data)
|
|
|
|
|
logdbg('packet: %s' % pkt)
|
|
|
|
|
if pkt:
|
|
|
|
|
yield pkt
|
|
|
|
|
except Queue.Empty:
|
|
|
|
|
logdbg('empty queue')
|
|
|
|
|
|
|
|
|
|
def data_to_packet(data):
|
|
|
|
|
def data_to_packet(self, data):
|
|
|
|
|
# convert netatmo data to format, units, and scaling for database
|
|
|
|
|
packet = {'dateTime': int(time.time() + 0.5), 'usUnits': weewx.METRIC}
|
|
|
|
|
for n in data:
|
|
|
|
|
if n in self.sensor_map:
|
|
|
|
|
packet[sensor_map[n]] = data[n]
|
|
|
|
|
packet = dict()
|
|
|
|
|
packet['dateTime'] = data.pop('dateTime', int(time.time() + 0.5))
|
|
|
|
|
packet['usUnits'] = data.pop('usUnits', weewx.US)
|
|
|
|
|
|
|
|
|
|
for n in self.sensor_map:
|
|
|
|
|
label = self._find_match(n, data.keys())
|
|
|
|
|
if label:
|
|
|
|
|
packet[self.sensor_map[n]] = data[label]
|
|
|
|
|
return packet
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _find_match(pattern, keylist):
|
|
|
|
|
pparts = pattern.split('.')
|
|
|
|
|
if len(pparts) != 3:
|
|
|
|
|
return None
|
|
|
|
|
for k in keylist:
|
|
|
|
|
kparts = k.split('.')
|
|
|
|
|
if (NetatmoDriver._part_match(pparts[0], kparts[0]) and
|
|
|
|
|
NetatmoDriver._part_match(pparts[1], kparts[1]) and
|
|
|
|
|
NetatmoDriver._part_match(pparts[2], kparts[2])):
|
|
|
|
|
return k
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _part_match(pattern, value):
|
|
|
|
|
if pattern == value:
|
|
|
|
|
return True
|
|
|
|
|
if pattern == '*' and value:
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Collector(object):
|
|
|
|
|
queue = Queue.Queue()
|
|
|
|
@ -117,168 +233,277 @@ class Collector(object):
|
|
|
|
|
class CloudClient(Collector):
|
|
|
|
|
"""Poll the netatmo servers for data. Put the result on the queue.
|
|
|
|
|
|
|
|
|
|
The netatmo server provides the following data:
|
|
|
|
|
noise is measured in dB
|
|
|
|
|
co2 is measured in ppm
|
|
|
|
|
rain is measured in mm (or inch?) not specified in docs!
|
|
|
|
|
temperatures are measured in C or F
|
|
|
|
|
|
|
|
|
|
the user object indicates the units of the download
|
|
|
|
|
unit: 0: metric, 1: imperial
|
|
|
|
|
windunit: 0: kph, 1: mph, 2: m/s, 3: beafort, 4: knot
|
|
|
|
|
pressureunit: 0: mbar, 1: inHg, 2: mmHg
|
|
|
|
|
lang: user locale
|
|
|
|
|
reg_locale: regional preferences for date
|
|
|
|
|
feel_like_algo: 0: humidex, 1: heat-index
|
|
|
|
|
|
|
|
|
|
rf_status is a mapping to rssi (+dB)
|
|
|
|
|
0: 90, 1: 80, 2: 70, 3: 60
|
|
|
|
|
wifi_status is a mapping to rssi (+dB)
|
|
|
|
|
0: 86, 1: 71, 2: 56
|
|
|
|
|
battery_vp measures battery capacity
|
|
|
|
|
indoor: 6000-4200; 0: 5640, 1: 5280, 2: 4920, 3: 4560
|
|
|
|
|
rain/outdoor: 6000-3600; 0: 5500, 1: 5000, 2: 4500, 3: 4000
|
|
|
|
|
wind: 6000-3950; 0: 5590, 1: 5180, 2: 4770, 3: 4360
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# endpoints for the cloud queries
|
|
|
|
|
NETATMO_URL = 'https://api.netatmo.net'
|
|
|
|
|
AUTH_URL = '/oauth2/token'
|
|
|
|
|
DATA_URL = '/api/getstationsdata'
|
|
|
|
|
|
|
|
|
|
# mapping between observation name and function used to convert it
|
|
|
|
|
CONVERSIONS = {
|
|
|
|
|
'Temperature': '_cvt_temperature',
|
|
|
|
|
'AbsolutePressure': '_cvt_pressure',
|
|
|
|
|
'Pressure': '_cvt_pressure',
|
|
|
|
|
'WindStrength': '_cvt_speed',
|
|
|
|
|
'GustStrength': '_cvt_speed',
|
|
|
|
|
'Rain': '_cvt_rain'}
|
|
|
|
|
|
|
|
|
|
# list of source units we need to watch for
|
|
|
|
|
UNITS = ['unit', 'windunit', 'pressureunit']
|
|
|
|
|
|
|
|
|
|
# these items are tracked from every module and every device
|
|
|
|
|
DASHBOARD_ITEMS = [
|
|
|
|
|
'Temperature', 'Humidity', 'AbsolutePressure', 'Pressure',
|
|
|
|
|
'CO2', 'Noise', 'Rain',
|
|
|
|
|
'WindStrength', 'WindAngle', 'GustStrength', 'GustAngle']
|
|
|
|
|
META_ITEMS = [
|
|
|
|
|
'wifi_status', 'rf_status', 'battery_vp', 'co2_calibrating',
|
|
|
|
|
'_id', 'module_name', 'last_status_store', 'last_seen',
|
|
|
|
|
'firmware', 'last_setup', 'last_upgrade', 'date_setup']
|
|
|
|
|
|
|
|
|
|
def __init__(self, username, password, client_id, client_secret,
|
|
|
|
|
device_id=None, poll_interval=600, max_tries=3, retry_wait=30):
|
|
|
|
|
self._poll_interval = poll_interval
|
|
|
|
|
self._max_tries = max_tries
|
|
|
|
|
self._retry_wait = retry_wait
|
|
|
|
|
self._device_id = device_id
|
|
|
|
|
self._auth = CloudClient.ClientAuth(
|
|
|
|
|
username, password, client_id, client_secret)
|
|
|
|
|
self._sd = CloudClient.StationData(self._auth)
|
|
|
|
|
self._thread = None
|
|
|
|
|
self._collect_data = False
|
|
|
|
|
|
|
|
|
|
def collect_data(self):
|
|
|
|
|
"""Loop forever, wake up periodically to see if it is time to quit."""
|
|
|
|
|
last_poll = 0
|
|
|
|
|
while self._collect_data:
|
|
|
|
|
now = int(time.time())
|
|
|
|
|
if now - last_poll > self._poll_interval:
|
|
|
|
|
for tries in range(self._max_tries):
|
|
|
|
|
try:
|
|
|
|
|
CloudClient.get_data(self._sd, self._device_id)
|
|
|
|
|
break
|
|
|
|
|
except urllib2.HTTPError, e:
|
|
|
|
|
logerr("failed attempt %s of %s to get data: %s" %
|
|
|
|
|
(tries + 1, self._max_tries, e))
|
|
|
|
|
logdbg("waiting %s seconds before retry" %
|
|
|
|
|
self.retry_wait)
|
|
|
|
|
else:
|
|
|
|
|
logerr("failed to get data after %d attempts" %
|
|
|
|
|
self._max_tries)
|
|
|
|
|
last_poll = now
|
|
|
|
|
logdbg('next update in %s seconds' % self._poll_interval)
|
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_data(sd, device_id):
|
|
|
|
|
"""Query the server for each device and module, put data on the queue"""
|
|
|
|
|
raw_data = sd.get_data(device_id)
|
|
|
|
|
units_dict = dict((x, raw_data['user']['administrative'][x])
|
|
|
|
|
for x in CloudClient.UNITS)
|
|
|
|
|
logdbg('cloud units: %s' % units_dict)
|
|
|
|
|
for d in raw_data['devices']:
|
|
|
|
|
data = CloudClient.extract_data(d, units_dict)
|
|
|
|
|
data = CloudClient.apply_labels(data, d['_id'], d['type'])
|
|
|
|
|
Collector.queue.put(data)
|
|
|
|
|
for m in d['modules']:
|
|
|
|
|
data = CloudClient.extract_data(m, units_dict)
|
|
|
|
|
data = CloudClient.apply_labels(data, m['_id'], m['type'])
|
|
|
|
|
Collector.queue.put(data)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def extract_data(x, units_dict):
|
|
|
|
|
"""Extract data we care about from a device or module"""
|
|
|
|
|
data = {'dateTime': x['dashboard_data']['time_utc'],
|
|
|
|
|
'usUnits': weewx.METRIC}
|
|
|
|
|
for n in CloudClient.META_ITEMS:
|
|
|
|
|
if n in x:
|
|
|
|
|
data[n] = x[n]
|
|
|
|
|
for n in CloudClient.DASHBOARD_ITEMS:
|
|
|
|
|
if n in x['dashboard_data']:
|
|
|
|
|
data[n] = x['dashboard_data'][n]
|
|
|
|
|
# do any unit conversions - everything converts to weewx.METRIC
|
|
|
|
|
for n in data:
|
|
|
|
|
try:
|
|
|
|
|
func = CloudClient.CONVERSIONS.get(n)
|
|
|
|
|
if func:
|
|
|
|
|
data[n] = getattr(CloudClient, func)(data[n], units_dict)
|
|
|
|
|
except ValueError, e:
|
|
|
|
|
logerr("unit conversion failed for %s: %s" % (data[n], e))
|
|
|
|
|
data[n] = None
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
NETATMO_URL = 'https://api.netatmo.net/'
|
|
|
|
|
AUTH_URL = NETATMO_URL + 'oauth2/token'
|
|
|
|
|
GETUSER_URL = NETATMO_URL + 'api/getuser'
|
|
|
|
|
DEVICELIST_URL = NETATMO_URL + 'api/devicelist'
|
|
|
|
|
GETMEASURE_URL = NETATMO_URL + 'api/getmeasure'
|
|
|
|
|
@staticmethod
|
|
|
|
|
def apply_labels(data, xid, xtype):
|
|
|
|
|
"""Copy the data dict but use fully-qualified keys"""
|
|
|
|
|
new_data = dict()
|
|
|
|
|
for n in data:
|
|
|
|
|
# do not touch the timestamp or units
|
|
|
|
|
if n in ['dateTime', 'usUnits']:
|
|
|
|
|
new_data[n] = data[n]
|
|
|
|
|
else:
|
|
|
|
|
new_data["%s.%s.%s" % (xid, xtype, n)] = data[n]
|
|
|
|
|
return new_data
|
|
|
|
|
|
|
|
|
|
def __init__(self, poll_interval):
|
|
|
|
|
self._poll_interval = poll_interval
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _cvt_pressure(x, from_unit_dict):
|
|
|
|
|
# pressureunit: 0: mbar, 1: inHg, 2: mmHg
|
|
|
|
|
if from_unit_dict['pressureunit'] == 1:
|
|
|
|
|
x /= INHG_PER_MBAR
|
|
|
|
|
elif from_unit_dict['pressureunit'] == 2:
|
|
|
|
|
x /= INHG_PER_MBAR * 25.4
|
|
|
|
|
return x
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _cvt_speed(x, from_unit_dict):
|
|
|
|
|
# windunit: 0: kph, 1: mph, 2: m/s, 3: beafort, 4: knot
|
|
|
|
|
if from_unit_dict['windunit'] == 1:
|
|
|
|
|
x *= MPH_TO_KPH
|
|
|
|
|
elif from_unit_dict['windunit'] == 2:
|
|
|
|
|
x *= MPS_TO_KPH
|
|
|
|
|
elif from_unit_dict['windunit'] == 3:
|
|
|
|
|
x = BEAFORT_TO_KPH.get(x)
|
|
|
|
|
elif from_unit_dict['windunit'] == 4:
|
|
|
|
|
x *= KNOT_TO_KPH
|
|
|
|
|
return x
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _cvt_temperature(x, from_unit_dict):
|
|
|
|
|
if from_unit_dict['unit'] == 1:
|
|
|
|
|
x = weewx.wxformulas.FtoC(x)
|
|
|
|
|
return x
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _cvt_rain(x, from_unit_dict):
|
|
|
|
|
# FIXME: verify that 0 is cm and 1 is inch
|
|
|
|
|
if from_unit_dict['unit'] == 1:
|
|
|
|
|
x *= CM_PER_IN
|
|
|
|
|
return x
|
|
|
|
|
|
|
|
|
|
def startup(self):
|
|
|
|
|
auth = ClientAuth(client_id, client_secret, username, password)
|
|
|
|
|
devices = DeviceList(auth)
|
|
|
|
|
while True:
|
|
|
|
|
latest = devices.last_data()
|
|
|
|
|
logdbg('latest: %s' % latest)
|
|
|
|
|
Collector.queue.put(latest)
|
|
|
|
|
time.sleep(self._poll_interval)
|
|
|
|
|
"""Start a thread that collects data from the netatmo servers."""
|
|
|
|
|
self._thread = CloudClient.CollectorThread(self)
|
|
|
|
|
self._collect_data = True
|
|
|
|
|
self._thread.start()
|
|
|
|
|
|
|
|
|
|
def shutdown(self):
|
|
|
|
|
"""Tell the thread to stop, then wait for it to finish."""
|
|
|
|
|
if self._thread:
|
|
|
|
|
self._collect_data = False
|
|
|
|
|
self._thread.join()
|
|
|
|
|
self._thread = None
|
|
|
|
|
|
|
|
|
|
class CollectorThread(threading.Thread):
|
|
|
|
|
def __init__(self, client):
|
|
|
|
|
threading.Thread.__init__(self)
|
|
|
|
|
self.client = client
|
|
|
|
|
self.name = 'netatmo-client'
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
self.client.collect_data()
|
|
|
|
|
|
|
|
|
|
class ClientAuth(object):
|
|
|
|
|
def __init__(self, client_id, client_secret, username, password):
|
|
|
|
|
"""Encapsulate the client authentication data and protocols. This
|
|
|
|
|
object contains the username, password, client_id, and client_secret
|
|
|
|
|
that are required to obtain authorization tokens from netatmo.com.
|
|
|
|
|
|
|
|
|
|
It will re-query the netatmo server whenever the tokens have expired"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, username, password, client_id, client_secret):
|
|
|
|
|
self._username = username
|
|
|
|
|
self._password = password
|
|
|
|
|
self._client_id = client_id
|
|
|
|
|
self._client_secret = client_secret
|
|
|
|
|
self._access_token = None
|
|
|
|
|
self._refresh_token = None
|
|
|
|
|
self._scope = None
|
|
|
|
|
self._expiration = None
|
|
|
|
|
|
|
|
|
|
def obtain_token(self):
|
|
|
|
|
params = {
|
|
|
|
|
'grant_type': 'password',
|
|
|
|
|
'client_id': client_id,
|
|
|
|
|
'client_secret': client_secret,
|
|
|
|
|
'username': username,
|
|
|
|
|
'password': password,
|
|
|
|
|
'client_id': self._client_id,
|
|
|
|
|
'client_secret': self._client_secret,
|
|
|
|
|
'username': self._username,
|
|
|
|
|
'password': self._password,
|
|
|
|
|
'scope': 'read_station'}
|
|
|
|
|
resp = CloudClient.post_request(self.AUTH_URL, params)
|
|
|
|
|
self._client_id = client_id
|
|
|
|
|
self._client_secret = client_secret
|
|
|
|
|
resp = CloudClient.post_request(CloudClient.AUTH_URL, params)
|
|
|
|
|
self._access_token = resp['access_token']
|
|
|
|
|
self._refresh_token = resp['refresh_token']
|
|
|
|
|
self._scope = resp['scope']
|
|
|
|
|
self._expiration = int(resp['expire_in'] + time.time())
|
|
|
|
|
# clear credentials cache
|
|
|
|
|
self._username = self._password = None
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def access_token(self):
|
|
|
|
|
if self._access_token is None:
|
|
|
|
|
self.obtain_token()
|
|
|
|
|
if self._access_token is None:
|
|
|
|
|
return None # FIXME: indicate failure
|
|
|
|
|
if self._expiration < time.time():
|
|
|
|
|
params = {
|
|
|
|
|
'grant_type': 'refresh_token',
|
|
|
|
|
'refresh_token': self._refresh_token,
|
|
|
|
|
'client_id': self._client_id,
|
|
|
|
|
'client_secret': self._client_secret}
|
|
|
|
|
resp = CloudClient.post_request(self.AUTH_URL, params)
|
|
|
|
|
resp = CloudClient.post_request(CloudClient.AUTH_URL, params)
|
|
|
|
|
self._access_token = resp['access_token']
|
|
|
|
|
self._refresh_token = resp['refresh_token']
|
|
|
|
|
self._expiration = int(resp['expire_in'] + time.time())
|
|
|
|
|
|
|
|
|
|
class User(object):
|
|
|
|
|
def __init__(self, auth_data):
|
|
|
|
|
params = {
|
|
|
|
|
'access_token': auth_data.access_token}
|
|
|
|
|
resp = CloudClient.post_request(GETUSER_URL, params)
|
|
|
|
|
self._raw_data = resp['body']
|
|
|
|
|
self._id = self._raw_data['_id']
|
|
|
|
|
self._devices = self._raw_data['devices']
|
|
|
|
|
self._ownermail = self._raw_data['mail']
|
|
|
|
|
|
|
|
|
|
class DeviceList(object):
|
|
|
|
|
def __init__(self, auth_data):
|
|
|
|
|
self._token = auth_data.access_token
|
|
|
|
|
params = {
|
|
|
|
|
'access_token': self._token,
|
|
|
|
|
'app_type': 'app_station'}
|
|
|
|
|
resp = CloudClient.post_request(DEVICELIST_URL, params)
|
|
|
|
|
self._raw_data = resp['body']
|
|
|
|
|
self._statiosn = {d['_id'] : d for d in self._raw_data['devices']}
|
|
|
|
|
self._modules = {m['_id'] : m for m in self._raw_data['modules']}
|
|
|
|
|
self._default = list(self._stations.values())[0]['station_name']
|
|
|
|
|
|
|
|
|
|
def module_names(self, station=None):
|
|
|
|
|
res = [m['module_name'] for m in self._modules.values()]
|
|
|
|
|
res.append(self.station_by_name(station)['module_name'])
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
def station_by_name(self, station=None):
|
|
|
|
|
if not station:
|
|
|
|
|
station = self._default_station
|
|
|
|
|
for i, s in self._stations.items():
|
|
|
|
|
if s['station_name'] == station:
|
|
|
|
|
return self._stations[i]
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def station_by_id(self, sid):
|
|
|
|
|
return None if sid not in self._stations else self._stations[sid]
|
|
|
|
|
|
|
|
|
|
def module_by_name(self, module, station=None):
|
|
|
|
|
s = None
|
|
|
|
|
if station:
|
|
|
|
|
s = self.station_by_name(station)
|
|
|
|
|
if not s:
|
|
|
|
|
return None
|
|
|
|
|
for m in self._modules:
|
|
|
|
|
mod = self._modules[m]
|
|
|
|
|
if mod['module_name'] == module:
|
|
|
|
|
if not s or mod['main_device'] == s['_id']:
|
|
|
|
|
return mod
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def module_by_id(self, mid, sid=None):
|
|
|
|
|
s = self.station_by_id(sid) if sid else None
|
|
|
|
|
if mid in self._modules:
|
|
|
|
|
if not s or self.modules[mid]['main_device'] == s['_id']:
|
|
|
|
|
return self.modules[mid]
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_measure(self, device_id, scale, mtype, module_id=None,
|
|
|
|
|
date_begin=None, date_end=None, limit=None,
|
|
|
|
|
optimize=False, real_time=False):
|
|
|
|
|
params = {
|
|
|
|
|
'access_token': self._token,
|
|
|
|
|
'device_id': device_id,
|
|
|
|
|
'scale': scale,
|
|
|
|
|
'type', mtype,
|
|
|
|
|
'optimize': 'true' if optimize else 'false',
|
|
|
|
|
'real_time': 'true' if real_time else 'false'}
|
|
|
|
|
if module_id:
|
|
|
|
|
params['module_id'] = module_id
|
|
|
|
|
if date_begin:
|
|
|
|
|
params['date_begin'] = date_begin
|
|
|
|
|
if date_end:
|
|
|
|
|
params['date_end'] = date_end
|
|
|
|
|
if limit:
|
|
|
|
|
params['limit'] = limit
|
|
|
|
|
return CloudClient.post_request(GETMEASURE_URL, params)
|
|
|
|
|
|
|
|
|
|
def last_data(self, station=None, exclude=0):
|
|
|
|
|
s = self.station_by_name(station)
|
|
|
|
|
if not s:
|
|
|
|
|
return None
|
|
|
|
|
data = dict()
|
|
|
|
|
limit = (time.time() - exclue) if exclude else 0
|
|
|
|
|
ds = s['dashboard_data']
|
|
|
|
|
if ds['time_utc'] > limit:
|
|
|
|
|
data[s['module_name']] = ds.copy()
|
|
|
|
|
data[s['module_name']]['When'] = data[s['module_name']].pop('time_utc')
|
|
|
|
|
data[s['module_name']]['wifi_status'] = s['wifi_status']
|
|
|
|
|
for mid in s['modules']:
|
|
|
|
|
ds = self._modules[mid]['dashboard_data']
|
|
|
|
|
if ds['time_utc'] > limit:
|
|
|
|
|
mod = self._modules[mid]
|
|
|
|
|
data[mod['module_name']] = ds.copy()
|
|
|
|
|
data[mod['module_name']]['When'] = data[mod['module_name']].pop('time_utc')
|
|
|
|
|
for i in ('battery_vp', 'rf_status'):
|
|
|
|
|
if i in mod:
|
|
|
|
|
data[mod['module_name']][i] = mod[i]
|
|
|
|
|
return data
|
|
|
|
|
return self._access_token
|
|
|
|
|
|
|
|
|
|
class StationData(object):
|
|
|
|
|
def __init__(self, auth):
|
|
|
|
|
self._auth = auth
|
|
|
|
|
self._last_update = 0
|
|
|
|
|
self._raw_data = dict()
|
|
|
|
|
|
|
|
|
|
def get_data(self, device_id=None, stale=600):
|
|
|
|
|
if int(time.time()) - self._last_update > stale:
|
|
|
|
|
params = {'access_token': self._auth.access_token}
|
|
|
|
|
if device_id:
|
|
|
|
|
params['device_id'] = device_id
|
|
|
|
|
resp = CloudClient.post_request(CloudClient.DATA_URL, params)
|
|
|
|
|
self._raw_data = dict(resp['body'])
|
|
|
|
|
self._last_update = int(time.time())
|
|
|
|
|
return self._raw_data
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def post_request(url, params):
|
|
|
|
|
# netatmo response body size is limited to 64K
|
|
|
|
|
url = CloudClient.NETATMO_URL + url
|
|
|
|
|
params = urlencode(params)
|
|
|
|
|
headers = {
|
|
|
|
|
"Content-Type": "application/x-www-form-urlencoded;charset=urf-8"}
|
|
|
|
|
logdbg("url: %s data: %s hdr: %s" % (url, params, headers))
|
|
|
|
|
req = urllib2.Request(url=url, data=params, headers=headers)
|
|
|
|
|
resp = urllib2.urlopen(req).read(65535)
|
|
|
|
|
return json.loads(resp)
|
|
|
|
|
resp_obj = json.loads(resp)
|
|
|
|
|
logdbg("resp_obj: %s" % resp_obj)
|
|
|
|
|
return resp_obj
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PacketSniffer(Collector):
|
|
|
|
@ -291,7 +516,7 @@ class PacketSniffer(Collector):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TCPPacket(object):
|
|
|
|
|
class Packet(object):
|
|
|
|
|
_HDR = re.compile('(\d+).(\d+) IP (\S+) > (\S+):')
|
|
|
|
|
_DATA = re.compile('0x00\d0: (.*)')
|
|
|
|
|
|
|
|
|
@ -303,7 +528,7 @@ class TCPPacket(object):
|
|
|
|
|
data = []
|
|
|
|
|
for line in lines:
|
|
|
|
|
line = line.strip()
|
|
|
|
|
TCPPacket._HDR.search(line)
|
|
|
|
|
PacketSniffer.Packet._HDR.search(line)
|
|
|
|
|
if m:
|
|
|
|
|
ts = m.group(1)
|
|
|
|
|
src = m.group(3)
|
|
|
|
@ -312,7 +537,7 @@ class TCPPacket(object):
|
|
|
|
|
pkts.append({'dateTime': ts, 'src': src, 'dst': dst,
|
|
|
|
|
'data': ''.join(data)})
|
|
|
|
|
continue
|
|
|
|
|
TCPPacket._DATA.search(line)
|
|
|
|
|
PacketSniffer.Packet._DATA.search(line)
|
|
|
|
|
if m:
|
|
|
|
|
data.append(m.group(1))
|
|
|
|
|
continue
|
|
|
|
@ -333,102 +558,93 @@ if __name__ == "__main__":
|
|
|
|
|
import optparse
|
|
|
|
|
syslog.openlog('wee_netatmo', syslog.LOG_PID | syslog.LOG_CONS)
|
|
|
|
|
parser = optparse.OptionParser(usage=usage)
|
|
|
|
|
parser.add_option('--test-sniff', dest='ts', action='store_true',
|
|
|
|
|
help='test the driver in packet sniff mode')
|
|
|
|
|
parser.add_option('--test-cloud', dest='tc', action='store_true',
|
|
|
|
|
help='test the driver in cloud client mode')
|
|
|
|
|
parser.add_option('--test-parse', dest='tp', action='store_true',
|
|
|
|
|
help='test the parser')
|
|
|
|
|
parser.add_option('--version', dest='version', action='store_true',
|
|
|
|
|
help='display driver version')
|
|
|
|
|
parser.add_option('--debug', dest='debug', action='store_true',
|
|
|
|
|
help='display diagnostic information while running')
|
|
|
|
|
parser.add_option('--run-sniff-driver', dest='ts', action='store_true',
|
|
|
|
|
help='run the driver in packet sniff mode')
|
|
|
|
|
parser.add_option('--run-cloud-driver', dest='tc', action='store_true',
|
|
|
|
|
help='run the driver in cloud client mode')
|
|
|
|
|
parser.add_option('--test-parse', dest='tp', metavar='FILENAME',
|
|
|
|
|
help='test the tcp packet parser')
|
|
|
|
|
parser.add_option('--username', dest='username', metavar='USERNAME',
|
|
|
|
|
help='username for cloud mode')
|
|
|
|
|
parser.add_option('--password', dest='password', metavar='PASSWORD',
|
|
|
|
|
help='password for cloud mode')
|
|
|
|
|
parser.add_option('--get-cloud_data', dest='data', action='store_true',
|
|
|
|
|
help='get all cloud data')
|
|
|
|
|
parser.add_option('--client-id', dest='ci', metavar='CLIENT_ID',
|
|
|
|
|
help='client_id for cloud mode')
|
|
|
|
|
parser.add_option('--client-secret', dest='cs', metavar='CLIENT_SECRET',
|
|
|
|
|
help='client_secret for cloud mode')
|
|
|
|
|
parser.add_option('--get-stn-data', dest='sdata', action='store_true',
|
|
|
|
|
help='get formatted station data from cloud')
|
|
|
|
|
parser.add_option('--get-json-data', dest='jdata', action='store_true',
|
|
|
|
|
help='get all cloud data as json response')
|
|
|
|
|
(opts, args) = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
if opts.debug:
|
|
|
|
|
syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG))
|
|
|
|
|
|
|
|
|
|
if opts.ts:
|
|
|
|
|
test_packet_driver()
|
|
|
|
|
run_packet_driver()
|
|
|
|
|
if opts.tc:
|
|
|
|
|
test_cloud_driver(opts.username, opts.password)
|
|
|
|
|
run_cloud_driver(opts.username, opts.password, opts.ci, opts.cs)
|
|
|
|
|
if opts.tp:
|
|
|
|
|
test_parse()
|
|
|
|
|
test_parse(options.tp)
|
|
|
|
|
if opts.sdata:
|
|
|
|
|
get_station_data(opts.username, opts.password, opts.ci, opts.cs)
|
|
|
|
|
if opts.jdata:
|
|
|
|
|
get_json_data(opts.username, opts.password, opts.ci, opts.cs)
|
|
|
|
|
|
|
|
|
|
def test_sniff_driver():
|
|
|
|
|
def run_sniff_driver():
|
|
|
|
|
import weeutil.weeutil
|
|
|
|
|
driver = NetatmoDriver({'mode': 'sniff'})
|
|
|
|
|
for pkt in driver.genLoopPackets():
|
|
|
|
|
print weeutil.weeutil.timestamp_to_string(pkt['dateTime']), pkt
|
|
|
|
|
|
|
|
|
|
def test_cloud_driver(username, password):
|
|
|
|
|
def run_cloud_driver(username, password, c_id, c_secret):
|
|
|
|
|
import weeutil.weeutil
|
|
|
|
|
driver = NetatmoDriver({'mode': 'cloud',
|
|
|
|
|
'username': username, 'password': password})
|
|
|
|
|
driver = None
|
|
|
|
|
try:
|
|
|
|
|
driver = NetatmoDriver(mode='cloud',
|
|
|
|
|
username=username, password=password,
|
|
|
|
|
client_id=c_id, client_secret=c_secret)
|
|
|
|
|
for pkt in driver.genLoopPackets():
|
|
|
|
|
print weeutil.weeutil.timestamp_to_string(pkt['dateTime']), pkt
|
|
|
|
|
|
|
|
|
|
def get_cloud_data(username, password):
|
|
|
|
|
auth = CloudClient.ClientAuth(username, password)
|
|
|
|
|
devices = CloudClient.DeviceList(auth)
|
|
|
|
|
for module, module_data in devices.last_data(exclue=3600).items():
|
|
|
|
|
print module
|
|
|
|
|
for sensor, value in module_data.items():
|
|
|
|
|
if sensor == 'When':
|
|
|
|
|
value = time.strftime("%Y.%m.%d %H:%M:%S",
|
|
|
|
|
time.localtime(value))
|
|
|
|
|
print "%30s: %s" % (sensor, value)
|
|
|
|
|
|
|
|
|
|
def test_parse():
|
|
|
|
|
tcp_lines = """1450840574.054884 IP 10.1.10.11.56280 > b31.netatmo.net.25050: P 52:56(4) ack 33 win 2968
|
|
|
|
|
0x0000: 4500 002c 7ba7 0000 fe06 f2f2 0a01 0a0b
|
|
|
|
|
0x0010: 3ed2 fb53 dbd8 61da f828 80ee d800 3574
|
|
|
|
|
0x0020: 5018 0b98 1bbe 0000 7601 0000 0000
|
|
|
|
|
1450840574.153985 IP b31.netatmo.net.25050 > 10.1.10.11.56280: P 33:37(4) ack 56 win 29200
|
|
|
|
|
0x0000: 4520 002c da5b 4000 3406 1e1f 3ed2 fb53
|
|
|
|
|
0x0010: 0a01 0a0b 61da dbd8 d800 3574 f828 80f2
|
|
|
|
|
0x0020: 5018 7210 cb42 0000 6000 0000 0000
|
|
|
|
|
1450840574.262671 IP 10.1.10.11.56280 > b31.netatmo.net.25050: P 56:202(146) ack 37 win 2964
|
|
|
|
|
0x0000: 4500 00ba c99d 0000 fe06 a46e 0a01 0a0b
|
|
|
|
|
0x0010: 3ed2 fb53 dbd8 61da f828 80f2 d800 3578
|
|
|
|
|
0x0020: 5018 0b94 3750 0000 6100 2500 c210 7a56
|
|
|
|
|
0x0030: 3730 3a65 653a 3530 3a30 363a 3834 3a37
|
|
|
|
|
0x0040: 3200 da00 0137 0fa5 2703 d227 0731 05b7
|
|
|
|
|
0x0050: 0761
|
|
|
|
|
1450840574.363385 IP b31.netatmo.net.25050 > 10.1.10.11.56280: P 37:41(4) ack 202 win 30016
|
|
|
|
|
0x0000: 4520 002c da5c 4000 3406 1e1e 3ed2 fb53
|
|
|
|
|
0x0010: 0a01 0a0b 61da dbd8 d800 3578 f828 8184
|
|
|
|
|
0x0020: 5018 7540 217d 0000 0600 0000 0000
|
|
|
|
|
1450840574.464212 IP 10.1.10.11.56280 > b31.netatmo.net.25050: P 202:443(241) ack 41 win 2960
|
|
|
|
|
0x0000: 4500 0119 25c4 0000 fe06 47e9 0a01 0a0b
|
|
|
|
|
0x0010: 3ed2 fb53 dbd8 61da f828 8184 d800 357c
|
|
|
|
|
0x0020: 5018 0b90 132d 0000 1000 0100 0507 00e8
|
|
|
|
|
0x0030: 002b 0100 0000 0032 3032 3a30 303a 3030
|
|
|
|
|
0x0040: 3a30 363a 3836 3a32 3836 a615 0000 2b00
|
|
|
|
|
0x0050: e811
|
|
|
|
|
1450840574.568157 IP b31.netatmo.net.25050 > 10.1.10.11.56280: P 41:45(4) ack 443 win 31088
|
|
|
|
|
0x0000: 4520 002c da5d 4000 3406 1e1d 3ed2 fb53
|
|
|
|
|
0x0010: 0a01 0a0b 61da dbd8 d800 357c f828 8275
|
|
|
|
|
0x0020: 5018 7970 1a58 0000 0800 0000 0000
|
|
|
|
|
1450840574.666496 IP 10.1.10.11.56280 > b31.netatmo.net.25050: P 443:447(4) ack 45 win 2956
|
|
|
|
|
0x0000: 4500 002c 4bce 0000 fe06 22cc 0a01 0a0b
|
|
|
|
|
0x0010: 3ed2 fb53 dbd8 61da f828 8275 d800 3580
|
|
|
|
|
0x0020: 5018 0b8c 8738 0000 0900 0000 0000
|
|
|
|
|
1450840574.770808 IP b31.netatmo.net.25050 > 10.1.10.11.56280: F 45:45(0) ack 447 win 31088
|
|
|
|
|
0x0000: 4520 0028 da5e 4000 3406 1e20 3ed2 fb53
|
|
|
|
|
0x0010: 0a01 0a0b 61da dbd8 d800 3580 f828 8279
|
|
|
|
|
0x0020: 5011 7970 225b 0000 0000 0000 0000
|
|
|
|
|
1450840574.870753 IP 10.1.10.11.56280 > b31.netatmo.net.25050: . ack 46 win 2955
|
|
|
|
|
0x0000: 4500 0028 7d95 0000 fe06 f108 0a01 0a0b
|
|
|
|
|
0x0010: 3ed2 fb53 dbd8 61da f828 8279 d800 3581
|
|
|
|
|
0x0020: 5010 0b8b 9040 0000 0000 0000 0000
|
|
|
|
|
1450840574.871685 IP 10.1.10.11.56280 > b31.netatmo.net.25050: F 447:447(0) ack 46 win 2955
|
|
|
|
|
0x0000: 4500 0028 0a80 0000 fe06 641e 0a01 0a0b
|
|
|
|
|
0x0010: 3ed2 fb53 dbd8 61da f828 8279 d800 3581
|
|
|
|
|
0x0020: 5011 0b8b 903f 0000 0000 0000 0000
|
|
|
|
|
1450840574.976182 IP b31.netatmo.net.25050 > 10.1.10.11.56280: . ack 448 win 31088
|
|
|
|
|
0x0000: 4520 0028 cf38 4000 3406 2946 3ed2 fb53
|
|
|
|
|
0x0010: 0a01 0a0b 61da dbd8 d800 3581 f828 827a
|
|
|
|
|
0x0020: 5010 7970 225a 0000 0000 0000 0000"""
|
|
|
|
|
print TCPPacket.lines2packets(tcp_lines)
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
driver.closePort()
|
|
|
|
|
|
|
|
|
|
def get_station_data(username, password, c_id, c_secret):
|
|
|
|
|
auth = CloudClient.ClientAuth(username, password, c_id, c_secret)
|
|
|
|
|
sd = CloudClient.StationData(auth)
|
|
|
|
|
ppv('station data', sd.get_data())
|
|
|
|
|
|
|
|
|
|
def get_json_data(username, password, c_id, c_secret):
|
|
|
|
|
auth = CloudClient.ClientAuth(username, password, c_id, c_secret)
|
|
|
|
|
params = {'access_token': auth.access_token, 'app_type': 'app_station'}
|
|
|
|
|
reply = CloudClient.post_request(CloudClient.DEVICELIST_URL, params)
|
|
|
|
|
print json.dumps(reply, sort_keys=True, indent=2)
|
|
|
|
|
|
|
|
|
|
def test_parse(filename):
|
|
|
|
|
lines = []
|
|
|
|
|
with open(filename, "r") as f:
|
|
|
|
|
while f:
|
|
|
|
|
lines.append(f.readline())
|
|
|
|
|
print PacketSniffer.Packet.lines2packets(''.join(lines))
|
|
|
|
|
|
|
|
|
|
def ppv(label, x, level=0):
|
|
|
|
|
"""pretty-print a variable, recursing if it is a dict"""
|
|
|
|
|
indent = ' '
|
|
|
|
|
if type(x) is dict:
|
|
|
|
|
print "%s%s" % (indent * level, label)
|
|
|
|
|
for n in x:
|
|
|
|
|
ppv(n, x[n], level=level+1)
|
|
|
|
|
elif type(x) is list:
|
|
|
|
|
print "%s[" % (indent * level)
|
|
|
|
|
for i, y in enumerate(x):
|
|
|
|
|
ppv("%s %s" % (label, i), y, level=level+1)
|
|
|
|
|
print "%s]" % (indent * level)
|
|
|
|
|
else:
|
|
|
|
|
print "%s%s=%s" % (indent * level, label, x)
|
|
|
|
|
|
|
|
|
|
main()
|
|
|
|
|