You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
weewx-netatmo/bin/user/netatmo.py

435 lines
16 KiB

#!/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()