commit
caa3ca80c2
@ -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…
Reference in new issue