Skip to content

Commit 8a76bd4

Browse files
committed
[Fix] First set of changes to include Netatmo home parameter (#43)
1 parent 0a0f343 commit 8a76bd4

File tree

4 files changed

+109
-80
lines changed

4 files changed

+109
-80
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ For more detailed information see http://dev.netatmo.com
77
I have no relation with the netatmo company, I wrote this because I needed it myself,
88
and published it to save time to anyone who would have same needs.
99

10+
Following the implementation of "Home" everywhere in the Netatmo API with various behavior, the library has been adjusted to include the home parameters in most calls.
11+
If you are using a single account with a single home and single weather station, the library has been implemented so that your code should continue to run without change.
12+
13+
If you have multiple homes or were supplying a station name in some method calls, you will have to adapt your code :
14+
- to supply a home name when looking for data for most class initializers
15+
- to use the new station name set by Netatmo (which is not your previously set value)
16+
17+
1018
### Install ###
1119

1220
To install lnetatmo simply run:
@@ -18,6 +26,9 @@ To install lnetatmo simply run:
1826
pip install lnetatmo
1927

2028
Depending on your permissions you might be required to use sudo.
29+
30+
It is a single file module, on platforms where you have limited access, you just have to clone the repo and take the lnetatmo.py in the same directory than your main program.
31+
2132
Once installed you can simple add lnetatmo to your python scripts by including:
2233

2334
import lnetatmo

lnetatmo.py

Lines changed: 82 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
PythonAPI Netatmo REST data access
1111
coding=utf-8
1212
"""
13+
14+
import warnings
15+
if __name__ == "__main__": warnings.filterwarnings("ignore") # For installation test only
16+
1317
from sys import version_info
1418
from os import getenv
1519
from os.path import expanduser, exists
1620
import platform
17-
import warnings
1821
import json, time
1922
import imghdr
2023
import warnings
@@ -156,6 +159,10 @@ class NoDevice( Exception ):
156159
pass
157160

158161

162+
class NoHome( Exception ):
163+
pass
164+
165+
159166
class AuthFailure( Exception ):
160167
pass
161168

@@ -251,34 +258,35 @@ class ThermostatData:
251258
252259
Args:
253260
authData (clientAuth): Authentication information with a working access Token
261+
home : Home name or id of the home who's thermostat belongs to
254262
"""
255-
def __init__(self, authData):
263+
def __init__(self, authData, home=None):
264+
265+
# I don't own a thermostat thus I am not able to test the Thermostat support
266+
warnings.warn("The Thermostat code is not tested due to the lack of test environment.\n" \
267+
"As Netatmo is continuously breaking API compatibility, risk that current bindings are wrong is high.\n" \
268+
"Please report found issues (https://github.com/philippelt/netatmo-api-python/issues)",
269+
RuntimeWarning )
270+
256271
self.getAuthToken = authData.accessToken
257272
postParams = {
258273
"access_token" : self.getAuthToken
259274
}
260275
resp = postRequest(_GETTHERMOSTATDATA_REQ, postParams)
261276
self.rawData = resp['body']['devices']
262277
if not self.rawData : raise NoDevice("No thermostat available")
263-
self.thermostat = { d['_id'] : d for d in self.rawData }
264-
for t,v in self.thermostat.items():
265-
v['name'] = v['station_name']
266-
for m in v['modules']:
267-
m['name'] = m['module_name']
268-
self.defaultThermostat = self.rawData[0]['station_name']
269-
self.defaultThermostatId = self.rawData[0]['_id']
270-
self.defaultModule = self.rawData[0]['modules'][0]
271-
272-
def getThermostat(self, name=None, tid=None):
273-
if tid:
274-
if tid in self.thermostat.keys():
275-
return self.thermostat[tid]
276-
else:
277-
return None
278-
elif name:
279-
for t in self.thermostat.values():
280-
if t['name'] == name: return t
281-
return None
278+
self.thermostatData = filter_home_data(rawData, home)
279+
if not self.thermostatData : raise NoHome("No home %s found" % home)
280+
self.thermostatData['name'] = self.thermostatData['home_name']
281+
for m in self.thermostatData['modules']:
282+
m['name'] = m['module_name']
283+
self.defaultThermostat = self.thermostatData['home_name']
284+
self.defaultThermostatId = self.thermostatData['_id']
285+
self.defaultModule = self.thermostatData['modules'][0]
286+
287+
def getThermostat(self, name=None):
288+
if ['name'] != name: return None
289+
else: return
282290
return self.thermostat[self.defaultThermostatId]
283291

284292
def moduleNamesList(self, name=None, tid=None):
@@ -299,23 +307,28 @@ class WeatherStationData:
299307
Args:
300308
authData (ClientAuth): Authentication information with a working access Token
301309
"""
302-
def __init__(self, authData):
310+
def __init__(self, authData, home=None, station=None):
303311
self.getAuthToken = authData.accessToken
304312
postParams = {
305313
"access_token" : self.getAuthToken
306314
}
307315
resp = postRequest(_GETSTATIONDATA_REQ, postParams)
308316
self.rawData = resp['body']['devices']
309317
# Weather data
310-
if not self.rawData : raise NoDevice("No weather station available")
311-
self.stations = { d['_id'] : d for d in self.rawData }
318+
if not self.rawData : raise NoDevice("No weather station in any homes")
319+
# Stations are no longer in the Netatmo API, keeping them for compatibility
320+
self.stations = { d['station_name'] : d for d in self.rawData }
321+
self.homes = { d['home_name'] : d["station_name"] for d in self.rawData }
322+
# Keeping the old behavior for default station name
323+
if station and station not in self.stations: raise NoDevice("No station with name %s" % station)
324+
self.default_station = station or list(self.stations.keys())[0]
325+
if home and home not in self.homes : raise NoHome("No home with name %s" % home)
326+
self.default_home = home or list(self.homes.keys())[0]
312327
self.modules = dict()
313-
for i in range(len(self.rawData)):
314-
# Loop on external modules if they are present
315-
if 'modules' in self.rawData[i]:
316-
for m in self.rawData[i]['modules']:
317-
self.modules[ m['_id'] ] = m
318-
self.default_station = list(self.stations.values())[0]['station_name']
328+
self.default_station_data = self.stationByName(self.default_station)
329+
if 'modules' in self.default_station_data:
330+
for m in self.default_station_data['modules']:
331+
self.modules[ m['_id'] ] = m
319332
# User data
320333
userData = resp['body']['user']
321334
self.user = UserInfo()
@@ -327,7 +340,7 @@ def __init__(self, authData):
327340
setattr(self.user, k, v)
328341

329342

330-
def modulesNamesList(self, station=None):
343+
def modulesNamesList(self, station=None, home=None):
331344
res = [m['module_name'] for m in self.modules.values()]
332345
res.append(self.stationByName(station)['module_name'])
333346
return res
@@ -340,31 +353,20 @@ def stationByName(self, station=None):
340353
return None
341354

342355
def stationById(self, sid):
343-
return None if sid not in self.stations else self.stations[sid]
356+
return self.stations.get(sid)
344357

345-
def moduleByName(self, module, station=None):
346-
s = None
347-
if station :
348-
s = self.stationByName(station)
349-
if not s : return None
358+
def moduleByName(self, module):
350359
for m in self.modules:
351360
mod = self.modules[m]
352361
if mod['module_name'] == module :
353362
return mod
354363
return None
355364

356-
def moduleById(self, mid, sid=None):
357-
s = self.stationById(sid) if sid else None
358-
if mid in self.modules :
359-
if s:
360-
for module in s['modules']:
361-
if module['_id'] == mid:
362-
return module
363-
else:
364-
return self.modules[mid]
365+
def moduleById(self, mid):
366+
return self.modules.get(mid)
365367

366-
def lastData(self, station=None, exclude=0):
367-
s = self.stationByName(station) or self.stationById(station)
368+
def lastData(self, exclude=0):
369+
s = self.default_station_data
368370
# Breaking change from Netatmo : dashboard_data no longer available if station lost
369371
if not s or 'dashboard_data' not in s : return None
370372
lastD = dict()
@@ -417,22 +419,16 @@ def getMeasure(self, device_id, scale, mtype, module_id=None, date_begin=None, d
417419
postParams['real_time'] = "true" if real_time else "false"
418420
return postRequest(_GETMEASURE_REQ, postParams)
419421

420-
def MinMaxTH(self, station=None, module=None, frame="last24"):
421-
if not station : station = self.default_station
422-
s = self.stationByName(station)
423-
if not s :
424-
s = self.stationById(station)
425-
if not s : return None
422+
def MinMaxTH(self, module=None, frame="last24"):
423+
s = self.default_station_data
426424
if frame == "last24":
427425
end = time.time()
428426
start = end - 24*3600 # 24 hours ago
429427
elif frame == "day":
430428
start, end = todayStamps()
431429
if module and module != s['module_name']:
432-
m = self.moduleByName(module, s['station_name'])
433-
if not m :
434-
m = self.moduleById(s['_id'], module)
435-
if not m : return None
430+
m = self.moduleById(module) or self.moduleByName(module)
431+
if not m : raise NoDevice("Can't find module %s" % module)
436432
# retrieve module's data
437433
resp = self.getMeasure(
438434
device_id = s['_id'],
@@ -470,7 +466,7 @@ class HomeData:
470466
Args:
471467
authData (ClientAuth): Authentication information with a working access Token
472468
"""
473-
def __init__(self, authData):
469+
def __init__(self, authData, home=None):
474470
self.getAuthToken = authData.accessToken
475471
postParams = {
476472
"access_token" : self.getAuthToken
@@ -480,7 +476,7 @@ def __init__(self, authData):
480476
# Collect homes
481477
self.homes = { d['id'] : d for d in self.rawData['homes'] }
482478
if not self.homes : raise NoDevice("No home available")
483-
self.default_home = list(self.homes.values())[0]['name']
479+
self.default_home = home or list(self.homes.values())[0]['name']
484480
# Split homes data by category
485481
self.persons = dict()
486482
self.events = dict()
@@ -754,6 +750,17 @@ class WelcomeData(HomeData):
754750

755751
# Utilities routines
756752

753+
754+
def filter_home_data(rawData, home):
755+
if home:
756+
# Find a home who's home id or name is the one requested
757+
for h in rawData:
758+
if h["home_name"] == home or h["home_id"] == home:
759+
return h
760+
return None
761+
# By default, the first home is returned
762+
return rawData[0]
763+
757764
def cameraCommand(cameraUrl, commande, parameters=None, timeout=3):
758765
url = cameraUrl + ( commande % parameters if parameters else commande)
759766
return postRequest(url, timeout=timeout)
@@ -798,25 +805,29 @@ def todayStamps():
798805

799806
# Global shortcut
800807

801-
def getStationMinMaxTH(station=None, module=None):
808+
def getStationMinMaxTH(station=None, module=None, home=None):
802809
authorization = ClientAuth()
803-
devList = DeviceList(authorization)
804-
if not station : station = devList.default_station
805-
if module :
806-
mname = module
807-
else :
808-
mname = devList.stationByName(station)['module_name']
809-
lastD = devList.lastData(station)
810-
if mname == "*":
810+
devList = WeatherStationData(authorization, station=station, home=home)
811+
if module == "*":
812+
pass
813+
elif module:
814+
module = devList.moduleById(module) or devList.moduleByName(module)
815+
if not module: raise NoDevice("No such module %s" % module)
816+
else: module = module["module_name"]
817+
else:
818+
module = list(devList.modules.values())[0]["module_name"]
819+
lastD = devList.lastData()
820+
if module == "*":
811821
result = dict()
812822
for m in lastD.keys():
813823
if time.time()-lastD[m]['When'] > 3600 : continue
814824
r = devList.MinMaxTH(module=m)
815825
result[m] = (r[0], lastD[m]['Temperature'], r[1])
816826
else:
817827
if time.time()-lastD[mname]['When'] > 3600 : result = ["-", "-"]
818-
else : result = [lastD[mname]['Temperature'], lastD[mname]['Humidity']]
819-
result.extend(devList.MinMaxTH(station, mname))
828+
else :
829+
result = [lastD[module]['Temperature'], lastD[module]['Humidity']]
830+
result.extend(devList.MinMaxTH(module))
820831
return result
821832

822833

@@ -841,7 +852,6 @@ def getStationMinMaxTH(station=None, module=None):
841852
else:
842853
weatherStation.MinMaxTH() # Test GETMEASUR
843854

844-
845855
try:
846856
homes = HomeData(authorization)
847857
except NoDevice :

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
setup(
66
name='lnetatmo',
7-
version='1.6.2',
7+
version='2.0.0',
88
classifiers=[
99
'Development Status :: 5 - Production/Stable',
1010
'Intended Audience :: Developers',
@@ -17,7 +17,7 @@
1717
scripts=[],
1818
data_files=[],
1919
url='https://github.com/philippelt/netatmo-api-python',
20-
download_url='https://github.com/philippelt/netatmo-api-python/tarball/v1.6.2.tar.gz',
20+
download_url='https://github.com/philippelt/netatmo-api-python/tarball/v2.0.0.tar.gz',
2121
license='GPL V3',
2222
description='Simple API to access Netatmo weather station data from any python script.'
2323
)

usage.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ Python Netatmo API programmers guide
77
88
>2014-01-13, Revision to include new modules additionnal informations
99
10-
>2016-06-25 Update documentation for Netatmo Welcome
10+
>2016-06-25, Update documentation for Netatmo Welcome
1111
12-
>2017-01-09 Minor updates to packaging info
12+
>2017-01-09, Minor updates to packaging info
13+
14+
>2020-12-07, Breaking changes due to removal of direct access to devices, "home" being now required (Netatmo redesign)
1315
1416
No additional library other than standard Python library is required.
1517

@@ -232,17 +234,22 @@ In most cases, you will not need to use this class that is oriented toward an ap
232234
Constructor
233235

234236
```python
235-
weatherData = lnetatmo.WeatherStationData( authorization )
237+
weatherData = lnetatmo.WeatherStationData( authorization, home=None, station=None )
236238
```
237239

238240

239-
Requires : an authorization object (ClientAuth instance)
240-
241+
* Input : : an authorization object (ClientAuth instance), an optional home name, an optional station name
241242

242243
Return : a WeatherStationData object. This object contains most administration properties of stations and modules accessible to the user and the last data pushed by the station to the Netatmo servers.
243244

244245
Raise a lnetatmo.NoDevice exception if no weather station is available for the given account.
245246

247+
If no home is specified, the first returned home will be set as default home. Same apply to station.
248+
If you have only one home and one station, you can safely ignore these new parameters. Note that return order is undefined. If you have multiple homes and a weather station in only one, the default home may be the one without station and the call will fail.
249+
250+
**Breaking change**
251+
> If you used the station name in the past in any method call, you should be aware that Netatmo decided to rename your station with their own value thus your existing code will have to be updated.
252+
246253
Properties, all properties are read-only unless specified:
247254

248255

@@ -443,8 +450,9 @@ Methods :
443450
If you just need the current temperature and humidity reported by a sensor with associated min and max values on the last 24 hours, you can get it all with only one call that handle all required steps including authentication :
444451

445452

446-
**getStationMinMaxTH**(station=None, module=None) :
453+
**getStationMinMaxTH**(station=None, module=Nonei, home=None) :
447454
* Input : optional station name and/or module name (if no station name is provided, default_station will be used, if no module name is provided, station sensor will be reported).
455+
if no home is specified, first returned home will be used
448456
* Output : A tuple of 6 values (Temperature, Humidity, minT, MaxT, minH, maxH)
449457

450458
```python

0 commit comments

Comments
 (0)