bring other changes in to git release

master
Matthew Wall 9 years ago
parent caa3ca80c2
commit 8274ff55ef

@ -1,11 +1,29 @@
#!/usr/bin/python #!/usr/bin/python
# $Id: netatmo.py 1489 2016-04-27 09:58:13Z mwall $
# Copyright 2015 Matthew Wall # Copyright 2015 Matthew Wall
# #
# Thanks to phillippe larduinat for publishing lnetatmo.py # Thanks to phillippe larduinat for publishing lnetatmo.py
# https://github.com/philippelt/netatmo-api-python # 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 # Shame on netatmo for making it very difficult to get data from the hardware
# without going through their servers. # 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 from __future__ import with_statement
import Queue import Queue
@ -21,9 +39,19 @@ import urllib2
import weewx.drivers import weewx.drivers
import weewx.engine import weewx.engine
import weewx.units 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): def logmsg(level, msg):
syslog.syslog(level, 'netatmo: %s: %s' % syslog.syslog(level, 'netatmo: %s: %s' %
@ -38,44 +66,105 @@ def loginf(msg):
def logerr(msg): def logerr(msg):
logmsg(syslog.LOG_ERR, msg) logmsg(syslog.LOG_ERR, msg)
def loader(config_dict, engine): def loader(config_dict, engine):
return NetatmoDriver(**config_dict[DRIVER_NAME]) 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): class NetatmoDriver(weewx.drivers.AbstractDevice):
DEFAULT_PORT = 80
DEFAULT_HOST = ''
# map from netatmo names to database schema names # map from netatmo names to database schema names
DEFAULT_SENSOR_MAP = { DEFAULT_SENSOR_MAP = {
'pressure': 'pressure', '*.NAMain.AbsolutePressure': 'pressure',
'temperature_in': 'inTemp', '*.NAMain.Temperature': 'inTemp',
'humidity_in': 'inHumidity', '*.NAMain.Humidity': 'inHumidity',
'temperature_out': 'outTemp', '*.NAMain.CO2': 'co2',
'humidity_out': 'outHumidity', '*.NAMain.Noise': 'noise',
'temperature_1': 'extraTemp1', '*.NAMain.wifi_status': 'wifi_status',
'humidity_1': 'extraHumid1', '*.NAModule1.Temperature': 'outTemp',
'temperature_2': 'extraTemp2', '*.NAModule1.Humidity': 'outHumidity',
'humidity_2': 'extraHumid2', '*.NAModule1.rf_status': 'rf_status',
'temperature_3': 'extraTemp3', '*.NAModule1.battery_vp': 'battery_vp',
'humidity_3': 'extraHumid3', '*.NAModule4.Temperature': 'extraTemp1',
'wind_speed': 'windSpeed', '*.NAModule4.Humidity': 'extraHumid1',
'wind_dir': 'windDir', '*.NAModule4.rf_status': 'rf_status',
'rain': 'rain', '*.NAModule4.battery_vp': 'battery_vp',
'co2': 'co2', '*.NAModule2.WindStrength': 'windSpeed',
'noise': 'noise'} '*.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): def __init__(self, **stn_dict):
loginf("driver version is %s" % DRIVER_VERSION) 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') mode = stn_dict.get('mode', 'cloud')
self.max_tries = int(stn_dict.get('max_tries', 5)) if mode.lower() == 'sniff':
self.retry_wait = int(stn_dict.get('retry_wait', 10)) # seconds port = int(stn_dict.get('port', NetatmoDriver.DEFAULT_PORT))
self.poll_interval = int(stn_dict.get('poll_interval', 600)) # seconds addr = stn_dict.get('host', NetatmoDriver.DEFAULT_HOST)
timeout = int(stn_dict.get('timeout', 3)) self.collector = PacketSniffer((addr, port))
port = int(stn_dict.get('port', 4200)) elif mode.lower() == 'cloud':
addr = stn_dict.get('host', '') max_tries = int(stn_dict.get('max_tries', 5))
if mode == 'sniff': retry_wait = int(stn_dict.get('retry_wait', 10)) # seconds
self.collector = PacketSniffer(addr, port) 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: else:
self.collector = CloudClient() raise ValueError("unsupported mode '%s'" % mode)
self.collector.startup() self.collector.startup()
def closePort(self): def closePort(self):
@ -89,20 +178,47 @@ class NetatmoDriver(weewx.drivers.AbstractDevice):
while True: while True:
try: try:
data = self.collector.queue.get(True, 10) data = self.collector.queue.get(True, 10)
logdbg('data: %s' % data)
pkt = self.data_to_packet(data) pkt = self.data_to_packet(data)
logdbg('packet: %s' % pkt)
if pkt: if pkt:
yield pkt yield pkt
except Queue.Empty: except Queue.Empty:
logdbg('empty queue') logdbg('empty queue')
def data_to_packet(data): def data_to_packet(self, data):
# convert netatmo data to format, units, and scaling for database # convert netatmo data to format, units, and scaling for database
packet = {'dateTime': int(time.time() + 0.5), 'usUnits': weewx.METRIC} packet = dict()
for n in data: packet['dateTime'] = data.pop('dateTime', int(time.time() + 0.5))
if n in self.sensor_map: packet['usUnits'] = data.pop('usUnits', weewx.US)
packet[sensor_map[n]] = data[n]
for n in self.sensor_map:
label = self._find_match(n, data.keys())
if label:
packet[self.sensor_map[n]] = data[label]
return packet 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): class Collector(object):
queue = Queue.Queue() queue = Queue.Queue()
@ -117,169 +233,278 @@ class Collector(object):
class CloudClient(Collector): class CloudClient(Collector):
"""Poll the netatmo servers for data. Put the result on the queue. """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
""" """
NETATMO_URL = 'https://api.netatmo.net/' # endpoints for the cloud queries
AUTH_URL = NETATMO_URL + 'oauth2/token' NETATMO_URL = 'https://api.netatmo.net'
GETUSER_URL = NETATMO_URL + 'api/getuser' AUTH_URL = '/oauth2/token'
DEVICELIST_URL = NETATMO_URL + 'api/devicelist' DATA_URL = '/api/getstationsdata'
GETMEASURE_URL = NETATMO_URL + 'api/getmeasure'
# mapping between observation name and function used to convert it
def __init__(self, poll_interval): 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._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
@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
@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): def startup(self):
auth = ClientAuth(client_id, client_secret, username, password) """Start a thread that collects data from the netatmo servers."""
devices = DeviceList(auth) self._thread = CloudClient.CollectorThread(self)
while True: self._collect_data = True
latest = devices.last_data() self._thread.start()
logdbg('latest: %s' % latest)
Collector.queue.put(latest) def shutdown(self):
time.sleep(self._poll_interval) """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): 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 = { params = {
'grant_type': 'password', 'grant_type': 'password',
'client_id': client_id, 'client_id': self._client_id,
'client_secret': client_secret, 'client_secret': self._client_secret,
'username': username, 'username': self._username,
'password': password, 'password': self._password,
'scope': 'read_station'} 'scope': 'read_station'}
resp = CloudClient.post_request(self.AUTH_URL, params) resp = CloudClient.post_request(CloudClient.AUTH_URL, params)
self._client_id = client_id
self._client_secret = client_secret
self._access_token = resp['access_token'] self._access_token = resp['access_token']
self._refresh_token = resp['refresh_token'] self._refresh_token = resp['refresh_token']
self._scope = resp['scope'] self._scope = resp['scope']
self._expiration = int(resp['expire_in'] + time.time()) self._expiration = int(resp['expire_in'] + time.time())
# clear credentials cache
self._username = self._password = None
@property @property
def access_token(self): 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(): if self._expiration < time.time():
params = { params = {
'grant_type': 'refresh_token', 'grant_type': 'refresh_token',
'refresh_token': self._refresh_token, 'refresh_token': self._refresh_token,
'client_id': self._client_id, 'client_id': self._client_id,
'client_secret': self._client_secret} '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._access_token = resp['access_token']
self._refresh_token = resp['refresh_token'] self._refresh_token = resp['refresh_token']
self._expiration = int(resp['expire_in'] + time.time()) self._expiration = int(resp['expire_in'] + time.time())
return self._access_token
class User(object):
def __init__(self, auth_data): class StationData(object):
params = { def __init__(self, auth):
'access_token': auth_data.access_token} self._auth = auth
resp = CloudClient.post_request(GETUSER_URL, params) self._last_update = 0
self._raw_data = resp['body'] self._raw_data = dict()
self._id = self._raw_data['_id']
self._devices = self._raw_data['devices'] def get_data(self, device_id=None, stale=600):
self._ownermail = self._raw_data['mail'] if int(time.time()) - self._last_update > stale:
params = {'access_token': self._auth.access_token}
class DeviceList(object): if device_id:
def __init__(self, auth_data): params['device_id'] = device_id
self._token = auth_data.access_token resp = CloudClient.post_request(CloudClient.DATA_URL, params)
params = { self._raw_data = dict(resp['body'])
'access_token': self._token, self._last_update = int(time.time())
'app_type': 'app_station'} return self._raw_data
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
@staticmethod @staticmethod
def post_request(url, params): def post_request(url, params):
# netatmo response body size is limited to 64K # netatmo response body size is limited to 64K
url = CloudClient.NETATMO_URL + url
params = urlencode(params) params = urlencode(params)
headers = { headers = {
"Content-Type": "application/x-www-form-urlencoded;charset=urf-8"} "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) req = urllib2.Request(url=url, data=params, headers=headers)
resp = urllib2.urlopen(req).read(65535) 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): class PacketSniffer(Collector):
"""listen for incoming packets then parse them. put result on queue.""" """listen for incoming packets then parse them. put result on queue."""
@ -291,37 +516,37 @@ class PacketSniffer(Collector):
pass pass
class TCPPacket(object): class Packet(object):
_HDR = re.compile('(\d+).(\d+) IP (\S+) > (\S+):') _HDR = re.compile('(\d+).(\d+) IP (\S+) > (\S+):')
_DATA = re.compile('0x00\d0: (.*)') _DATA = re.compile('0x00\d0: (.*)')
def lines2packets(lines): def lines2packets(lines):
pkts = [] pkts = []
ts = None ts = None
src = None src = None
dst = None dst = None
data = [] data = []
for line in lines: for line in lines:
line = line.strip() line = line.strip()
TCPPacket._HDR.search(line) PacketSniffer.Packet._HDR.search(line)
if m: if m:
ts = m.group(1) ts = m.group(1)
src = m.group(3) src = m.group(3)
dst = m.group(4) dst = m.group(4)
data = [] data = []
pkts.append({'dateTime': ts, 'src': src, 'dst': dst, pkts.append({'dateTime': ts, 'src': src, 'dst': dst,
'data': ''.join(data)}) 'data': ''.join(data)})
continue continue
TCPPacket._DATA.search(line) PacketSniffer.Packet._DATA.search(line)
if m: if m:
data.append(m.group(1)) data.append(m.group(1))
continue continue
return pkts return pkts
@staticmethod @staticmethod
def parse_data(data): def parse_data(data):
pkt = dict() pkt = dict()
return pkt return pkt
# To test this driver, do the following: # To test this driver, do the following:
@ -333,102 +558,93 @@ if __name__ == "__main__":
import optparse import optparse
syslog.openlog('wee_netatmo', syslog.LOG_PID | syslog.LOG_CONS) syslog.openlog('wee_netatmo', syslog.LOG_PID | syslog.LOG_CONS)
parser = optparse.OptionParser(usage=usage) parser = optparse.OptionParser(usage=usage)
parser.add_option('--test-sniff', dest='ts', action='store_true', parser.add_option('--version', dest='version', action='store_true',
help='test the driver in packet sniff mode') help='display driver version')
parser.add_option('--test-cloud', dest='tc', action='store_true', parser.add_option('--debug', dest='debug', action='store_true',
help='test the driver in cloud client mode') help='display diagnostic information while running')
parser.add_option('--test-parse', dest='tp', action='store_true', parser.add_option('--run-sniff-driver', dest='ts', action='store_true',
help='test the parser') 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', parser.add_option('--username', dest='username', metavar='USERNAME',
help='username for cloud mode') help='username for cloud mode')
parser.add_option('--password', dest='password', metavar='PASSWORD', parser.add_option('--password', dest='password', metavar='PASSWORD',
help='password for cloud mode') help='password for cloud mode')
parser.add_option('--get-cloud_data', dest='data', action='store_true', parser.add_option('--client-id', dest='ci', metavar='CLIENT_ID',
help='get all cloud data') 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() (opts, args) = parser.parse_args()
if opts.debug:
syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG))
if opts.ts: if opts.ts:
test_packet_driver() run_packet_driver()
if opts.tc: if opts.tc:
test_cloud_driver(opts.username, opts.password) run_cloud_driver(opts.username, opts.password, opts.ci, opts.cs)
if opts.tp: 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 import weeutil.weeutil
driver = NetatmoDriver({'mode': 'sniff'}) driver = NetatmoDriver({'mode': 'sniff'})
for pkt in driver.genLoopPackets(): for pkt in driver.genLoopPackets():
print weeutil.weeutil.timestamp_to_string(pkt['dateTime']), pkt 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 import weeutil.weeutil
driver = NetatmoDriver({'mode': 'cloud', driver = None
'username': username, 'password': password}) try:
for pkt in driver.genLoopPackets(): driver = NetatmoDriver(mode='cloud',
print weeutil.weeutil.timestamp_to_string(pkt['dateTime']), pkt username=username, password=password,
client_id=c_id, client_secret=c_secret)
def get_cloud_data(username, password): for pkt in driver.genLoopPackets():
auth = CloudClient.ClientAuth(username, password) print weeutil.weeutil.timestamp_to_string(pkt['dateTime']), pkt
devices = CloudClient.DeviceList(auth) except KeyboardInterrupt:
for module, module_data in devices.last_data(exclue=3600).items(): driver.closePort()
print module
for sensor, value in module_data.items(): def get_station_data(username, password, c_id, c_secret):
if sensor == 'When': auth = CloudClient.ClientAuth(username, password, c_id, c_secret)
value = time.strftime("%Y.%m.%d %H:%M:%S", sd = CloudClient.StationData(auth)
time.localtime(value)) ppv('station data', sd.get_data())
print "%30s: %s" % (sensor, value)
def get_json_data(username, password, c_id, c_secret):
def test_parse(): auth = CloudClient.ClientAuth(username, password, c_id, c_secret)
tcp_lines = """1450840574.054884 IP 10.1.10.11.56280 > b31.netatmo.net.25050: P 52:56(4) ack 33 win 2968 params = {'access_token': auth.access_token, 'app_type': 'app_station'}
0x0000: 4500 002c 7ba7 0000 fe06 f2f2 0a01 0a0b reply = CloudClient.post_request(CloudClient.DEVICELIST_URL, params)
0x0010: 3ed2 fb53 dbd8 61da f828 80ee d800 3574 print json.dumps(reply, sort_keys=True, indent=2)
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 def test_parse(filename):
0x0000: 4520 002c da5b 4000 3406 1e1f 3ed2 fb53 lines = []
0x0010: 0a01 0a0b 61da dbd8 d800 3574 f828 80f2 with open(filename, "r") as f:
0x0020: 5018 7210 cb42 0000 6000 0000 0000 while f:
1450840574.262671 IP 10.1.10.11.56280 > b31.netatmo.net.25050: P 56:202(146) ack 37 win 2964 lines.append(f.readline())
0x0000: 4500 00ba c99d 0000 fe06 a46e 0a01 0a0b print PacketSniffer.Packet.lines2packets(''.join(lines))
0x0010: 3ed2 fb53 dbd8 61da f828 80f2 d800 3578
0x0020: 5018 0b94 3750 0000 6100 2500 c210 7a56 def ppv(label, x, level=0):
0x0030: 3730 3a65 653a 3530 3a30 363a 3834 3a37 """pretty-print a variable, recursing if it is a dict"""
0x0040: 3200 da00 0137 0fa5 2703 d227 0731 05b7 indent = ' '
0x0050: 0761 if type(x) is dict:
1450840574.363385 IP b31.netatmo.net.25050 > 10.1.10.11.56280: P 37:41(4) ack 202 win 30016 print "%s%s" % (indent * level, label)
0x0000: 4520 002c da5c 4000 3406 1e1e 3ed2 fb53 for n in x:
0x0010: 0a01 0a0b 61da dbd8 d800 3578 f828 8184 ppv(n, x[n], level=level+1)
0x0020: 5018 7540 217d 0000 0600 0000 0000 elif type(x) is list:
1450840574.464212 IP 10.1.10.11.56280 > b31.netatmo.net.25050: P 202:443(241) ack 41 win 2960 print "%s[" % (indent * level)
0x0000: 4500 0119 25c4 0000 fe06 47e9 0a01 0a0b for i, y in enumerate(x):
0x0010: 3ed2 fb53 dbd8 61da f828 8184 d800 357c ppv("%s %s" % (label, i), y, level=level+1)
0x0020: 5018 0b90 132d 0000 1000 0100 0507 00e8 print "%s]" % (indent * level)
0x0030: 002b 0100 0000 0032 3032 3a30 303a 3030 else:
0x0040: 3a30 363a 3836 3a32 3836 a615 0000 2b00 print "%s%s=%s" % (indent * level, label, x)
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)
main() main()

@ -1,11 +1,22 @@
netatmo - weewx driver for netatmo weather stations netatmo - weewx driver for netatmo weather stations
Copyright 2015 Matthew Wall Copyright 2015 Matthew Wall
This driver has two modes of operation. It can use the Netatmo API to obtain This driver has two modes of operation. It can use the netatmo API to obtain
data from the netatmo servers, or it can parse the packets sent from a netatmo data from the netatmo servers, or it can parse the packets sent from a netatmo
station. The latter works only with netatmo firmware 102 (circa early 2015). station. The latter works only with netatmo firmware 101 (circa early 2015).
Firmware 102 introduced arbitrary encryption in response to a poorly chosen
decision to include the wifi password in network traffic sent to the netatmo
servers. Unfortunately, the encryption blocks end-users from accessing their
data, while the netatmo stations might still send information such as the wifi
password to the netatmo servers.
By default this driver will obtain data from the netatmo servers. By default this driver will operate in 'cloud' mode.
Communication with the netatmo servers requires 4 things: username, password,
client_id, and client_secret. The username and password are the credentials
used to access data at netatmo.com. The client_id and client_secret must be
obtained via the dev.netatmo.com web site. Using these 4 things, the driver
automatically obtains and updates the tokens needed to get data from the server.
Installation instructions: Installation instructions:
@ -19,6 +30,8 @@ wee_extension --install weewx-netatmo.tgz
[Netatmo] [Netatmo]
username = INSERT_USERNAME_HERE username = INSERT_USERNAME_HERE
password = INSERT_PASSWORD_HERE password = INSERT_PASSWORD_HERE
client_id = INSERT_CLIENT_ID_HERE
client_secret = INSERT_CLIENT_SECRET_HERE
3) start weewx: 3) start weewx:

Loading…
Cancel
Save