initial publish to github

master
Matthew Wall 9 years ago
commit caa3ca80c2

2
.gitignore vendored

@ -0,0 +1,2 @@
*~
*.pyc

@ -0,0 +1,434 @@
#!/usr/bin/python
# Copyright 2015 Matthew Wall
#
# Thanks to phillippe larduinat for publishing lnetatmo.py
# https://github.com/philippelt/netatmo-api-python
#
# Shame on netatmo for making it very difficult to get data from the hardware
# without going through their servers.
from __future__ import with_statement
import Queue
import json
import re
import socket
import syslog
import threading
import time
from urllib import urlencode
import urllib2
import weewx.drivers
import weewx.engine
import weewx.units
DRIVER_NAME = 'Netatmo'
DRIVER_VERSION = "0.2"
def logmsg(level, msg):
syslog.syslog(level, 'netatmo: %s: %s' %
(threading.currentThread().getName(), msg))
def logdbg(msg):
logmsg(syslog.LOG_DEBUG, msg)
def loginf(msg):
logmsg(syslog.LOG_INFO, msg)
def logerr(msg):
logmsg(syslog.LOG_ERR, msg)
def loader(config_dict, engine):
return NetatmoDriver(**config_dict[DRIVER_NAME])
class NetatmoDriver(weewx.drivers.AbstractDevice):
# 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'}
def __init__(self, **stn_dict):
loginf("driver version is %s" % DRIVER_VERSION)
self.sensor_map = stn_dict.get('sensor_map', NetatmoDriver.DEFAULT_SENSOR_MAP)
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)
else:
self.collector = CloudClient()
self.collector.startup()
def closePort(self):
self.collector.shutdown()
@property
def hardware_name(self):
return DRIVER_NAME
def genLoopPackets(self):
while True:
try:
data = self.collector.queue.get(True, 10)
pkt = self.data_to_packet(data)
if pkt:
yield pkt
except Queue.Empty:
logdbg('empty queue')
def data_to_packet(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]
return packet
class Collector(object):
queue = Queue.Queue()
def startup(self):
pass
def shutdown(self):
pass
class CloudClient(Collector):
"""Poll the netatmo servers for data. Put the result on the queue.
The netatmo server provides the following 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'
def __init__(self, poll_interval):
self._poll_interval = poll_interval
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)
class ClientAuth(object):
def __init__(self, client_id, client_secret, username, password):
params = {
'grant_type': 'password',
'client_id': client_id,
'client_secret': client_secret,
'username': username,
'password': password,
'scope': 'read_station'}
resp = CloudClient.post_request(self.AUTH_URL, params)
self._client_id = client_id
self._client_secret = client_secret
self._access_token = resp['access_token']
self._refresh_token = resp['refresh_token']
self._scope = resp['scope']
self._expiration = int(resp['expire_in'] + time.time())
@property
def access_token(self):
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)
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
@staticmethod
def post_request(url, params):
# netatmo response body size is limited to 64K
params = urlencode(params)
headers = {
"Content-Type": "application/x-www-form-urlencoded;charset=urf-8"}
req = urllib2.Request(url=url, data=params, headers=headers)
resp = urllib2.urlopen(req).read(65535)
return json.loads(resp)
class PacketSniffer(Collector):
"""listen for incoming packets then parse them. put result on queue."""
def startup(self):
pass
def shutdown(self):
pass
class TCPPacket(object):
_HDR = re.compile('(\d+).(\d+) IP (\S+) > (\S+):')
_DATA = re.compile('0x00\d0: (.*)')
def lines2packets(lines):
pkts = []
ts = None
src = None
dst = None
data = []
for line in lines:
line = line.strip()
TCPPacket._HDR.search(line)
if m:
ts = m.group(1)
src = m.group(3)
dst = m.group(4)
data = []
pkts.append({'dateTime': ts, 'src': src, 'dst': dst,
'data': ''.join(data)})
continue
TCPPacket._DATA.search(line)
if m:
data.append(m.group(1))
continue
return pkts
@staticmethod
def parse_data(data):
pkt = dict()
return pkt
# To test this driver, do the following:
# PYTHONPATH=bin python user/netatmo.py
if __name__ == "__main__":
usage = """%prog [options] [--help]"""
def 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('--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')
(opts, args) = parser.parse_args()
if opts.ts:
test_packet_driver()
if opts.tc:
test_cloud_driver(opts.username, opts.password)
if opts.tp:
test_parse()
def test_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):
import weeutil.weeutil
driver = NetatmoDriver({'mode': 'cloud',
'username': username, 'password': password})
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)
main()

@ -0,0 +1,5 @@
0.2 25apr2016
* release of combined driver
0.1 17jun2015
* initial public release

@ -0,0 +1,29 @@
# $Id: install.py 1484 2016-04-25 16:20:31Z mwall $
# installer for netatmo driver
# Copyright 2015 Matthew Wall
from setup import ExtensionInstaller
def loader():
return NetatmoInstaller()
class NetatmoInstaller(ExtensionInstaller):
def __init__(self):
super(NetatmoInstaller, self).__init__(
version="0.2",
name='netatmo',
description='Driver for netatmo weather stations.',
author="Matthew Wall",
author_email="mwall@users.sourceforge.net",
config={
'Station': {
'station_type': 'netatmo'},
'netatmo': {
'mode': 'cloud',
'username': 'INSERT_USERNAME_HERE',
'password': 'INSERT_PASSWORD_HERE',
'client_id': 'INSERT_CLIENT_ID_HERE',
'client_secret': 'INSERT_CLIENT_SECRET_HERE',
'driver': 'user.netatmo'}},
files=[('bin/user', ['bin/user/netatmo.py'])]
)

@ -0,0 +1,25 @@
netatmo - weewx driver for netatmo weather stations
Copyright 2015 Matthew Wall
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
station. The latter works only with netatmo firmware 102 (circa early 2015).
By default this driver will obtain data from the netatmo servers.
Installation instructions:
1) run the installer:
wee_extension --install weewx-netatmo.tgz
2) modify weewx.conf:
[Netatmo]
username = INSERT_USERNAME_HERE
password = INSERT_PASSWORD_HERE
3) start weewx:
sudo /etc/init.d/weewx start
Loading…
Cancel
Save