Skip to content

Commit e6c7c0d

Browse files
committed
[Change] Refactoring of authentication
1 parent 93b134c commit e6c7c0d

File tree

4 files changed

+69
-54
lines changed

4 files changed

+69
-54
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ I am trying to make this library survive to continuous Netatmo changes but their
1313
1414
>NEW MAJOR BREAKING CHANGE (december 2023): Web generated refresh_tokens are no more long lived tokens, they will be automatically refreshed. Consequences : No more static authentication in the library source and ~/.netatmo-credentials file will be updated to reflect change in the refresh token. This file MUST be writable and if you run Netatmo tools in container, remember to persist this file between container run. **This new token policy will completely forbid you to use your credentials on two or more systems if you can't share the .netatmo-credentials file**.
1515
16+
There is no longer credential load at library import, credentials are loaded at `ClientAuth` class initialization and a new parameter `credentialFile` allow to specify private name and location for the credential file.
17+
1618
### Install ###
1719

1820
To install lnetatmo simply run:

lnetatmo.py

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -43,25 +43,12 @@
4343
# To ease Docker packaging of your application, you can setup your authentication parameters through env variables
4444

4545
# Authentication:
46-
# 1 - The .netatmo.credentials file in JSON format in your home directory (now mandatory for regular use)
47-
# 2 - Values defined in environment variables : CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN
46+
# 1 - Values defined in environment variables : CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN
47+
# 2 - Parameters passed to ClientAuth initialization
48+
# 3 - The .netatmo.credentials file in JSON format in your home directory (now mandatory for regular use)
4849

4950
# Note that the refresh token being short lived, using envvar will be restricted to speific testing use case
5051

51-
# Note: this file will be rewritten by the library to record refresh_token change
52-
# If you run your application in container, remember to persist this file
53-
CREDENTIALS = expanduser("~/.netatmo.credentials")
54-
with open(CREDENTIALS, "r") as f:
55-
cred = {k.upper():v for k,v in json.loads(f.read()).items()}
56-
57-
def getParameter(key, default):
58-
return getenv(key, default.get(key, None))
59-
60-
# Override values with content of env variables if defined
61-
_CLIENT_ID = getParameter("CLIENT_ID", cred)
62-
_CLIENT_SECRET = getParameter("CLIENT_SECRET", cred)
63-
_REFRESH_TOKEN = getParameter("REFRESH_TOKEN", cred)
64-
6552
#########################################################################
6653

6754

@@ -225,14 +212,28 @@ class ClientAuth:
225212
refreshToken (str) : Scoped refresh token
226213
"""
227214

228-
def __init__(self, clientId=_CLIENT_ID,
229-
clientSecret=_CLIENT_SECRET,
230-
refreshToken=_REFRESH_TOKEN):
231-
232-
self._clientId = clientId
233-
self._clientSecret = clientSecret
234-
self._accessToken = None
235-
self.refreshToken = refreshToken
215+
def __init__(self, clientId=None,
216+
clientSecret=None,
217+
refreshToken=None,
218+
credentialFile=None):
219+
220+
# replace values with content of env variables if defined
221+
clientId = getenv("CLIENT_ID", clientId)
222+
clientSecret = getenv("CLIENT_SECRET", clientSecret)
223+
refreshToken = getenv("REFRESH_TOKEN", refreshToken)
224+
225+
# Look for credentials in file if not already provided
226+
# Note: this file will be rewritten by the library to record refresh_token change
227+
# If you run your application in container, remember to persist this file
228+
if not (clientId and clientSecret and refreshToken):
229+
credentialFile = credentialFile or expanduser("~/.netatmo.credentials")
230+
self._credentialFile = credentialFile
231+
with open(self._credentialFile, "r") as f:
232+
cred = {k.upper():v for k,v in json.loads(f.read()).items()}
233+
234+
self._clientId = clientId or cred["CLIENT_ID"]
235+
self._clientSecret = clientSecret or cred["CLIENT_SECRET"]
236+
self.refreshToken = refreshToken or cred["REFRESH_TOKEN"]
236237
self.expiration = 0 # Force refresh token
237238

238239
@property
@@ -250,8 +251,10 @@ def renew_token(self):
250251
resp = postRequest("authentication", _AUTH_REQ, postParams)
251252
if self.refreshToken != resp['refresh_token']:
252253
self.refreshToken = resp['refresh_token']
253-
cred["REFRESH_TOKEN"] = self.refreshToken
254-
with open(CREDENTIALS, "w") as f:
254+
cred = {"CLIENT_ID":self._clientId,
255+
"CLIENT_SECRET":self._clientSecret,
256+
"REFRESH_TOKEN":self.refreshToken }
257+
with open(self._credentialFile, "w") as f:
255258
f.write(json.dumps(cred, indent=True))
256259
self._accessToken = resp['access_token']
257260
self.expiration = int(resp['expire_in'] + time.time())
@@ -451,10 +454,16 @@ def __init__(self, authData, home=None, station=None):
451454
else:
452455
setattr(self.user, k, v)
453456

454-
455-
def modulesNamesList(self, station=None, home=None):
457+
def modulesNamesList(self, station=None):
458+
s = self.getStation(station)
459+
if not s: raise NoDevice("No station with name or id %s" % station)
460+
self.default_station = station
461+
self.default_station_data = s
462+
self.modules = dict()
463+
if 'modules' in self.default_station_data:
464+
for m in self.default_station_data['modules']:
465+
self.modules[ m['_id'] ] = m
456466
res = [m['module_name'] for m in self.modules.values()]
457-
station = self.stationByName(station) or self.stationById(station)
458467
res.append(station['module_name'])
459468
return res
460469

@@ -468,21 +477,21 @@ def getStation(self, station=None):
468477
if station in self.stationIds : return self.stationIds[station]
469478
return None
470479

480+
def getModule(self, module):
481+
if module in self.modules: return self.modules[module]
482+
for m in self.modules.values():
483+
if m['module_name'] == module : return m
484+
return None
485+
471486
# Functions for compatibility with previous versions
472487
def stationByName(self, station=None):
473488
return self.getStation(station)
474489
def stationById(self, sid):
475490
return self.getStation(sid)
476-
477491
def moduleByName(self, module):
478-
for m in self.modules:
479-
mod = self.modules[m]
480-
if mod['module_name'] == module :
481-
return mod
482-
return None
483-
492+
return self.getModule(module)
484493
def moduleById(self, mid):
485-
return self.modules.get(mid)
494+
return self.getModule(mid)
486495

487496
def lastData(self, station=None, exclude=0):
488497
s = self.stationByName(station) or self.stationById(station)
@@ -1082,10 +1091,6 @@ def getStationMinMaxTH(station=None, module=None, home=None):
10821091

10831092
logging.basicConfig(format='%(name)s - %(levelname)s: %(message)s', level=logging.INFO)
10841093

1085-
if not _CLIENT_ID or not _CLIENT_SECRET or not _REFRESH_TOKEN :
1086-
stderr.write("Library source missing identification arguments to check lnetatmo.py (user/password/etc...)")
1087-
exit(1)
1088-
10891094
authorization = ClientAuth() # Test authentication method
10901095

10911096
try:

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='4.0.1',
7+
version='4.1.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/archive/v4.0.1.tar.gz',
20+
download_url='https://github.com/philippelt/netatmo-api-python/archive/v4.1.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: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Python Netatmo API programmers guide
1919
2020
>2023-12-04, New update to Netatmo authentication rules, no longer long lived refresh token -> credentials MUST be writable, Hard coding credentials in the library no longer possible (bad luck for small home automation device)
2121
22+
>2024-01-03, New authentication method priorities, credential file as a parameter
23+
2224
No additional library other than standard Python library is required.
2325

2426
Both Python V2.7x and V3.x.x are supported without change.
@@ -50,31 +52,36 @@ Copy the lnetatmo.py file in your work directory (or use pip install lnetatmo).
5052

5153
Authentication data can be supplied with 3 different methods (each method override any settings of previous methods) :
5254

53-
1. Some or all values can stored in ~/.netatmo.credentials (in your platform home directory) file containing the keys in JSON format
55+
1. Some or all values can be defined by explicit call to initializer of ClientAuth class
56+
57+
̀```bash
58+
# Example: REFRESH_TOKEN supposed to be defined by an other method
59+
authData = lnetatmo.ClientAuth( clientId="netatmo-client-id",
60+
clientSecret="secret" )
61+
```
62+
63+
2. Some or all values can stored in ~/.netatmo.credentials (in your platform home directory) or a file path specified using the ̀`credentialFile` parameter. The file containing the keys in JSON format
5464

65+
̀̀```bash
5566
$ cat .netatmo.credentials # Here all values are defined but it is not mandatory
5667
{
5768
"CLIENT_ID" : "`xxx",
5869
"CLIENT_SECRET" : "xxx",
5970
"REFRESH_TOKEN" : "xxx"
6071
}
6172
$
73+
̀̀```
6274

63-
> Due to Netatmo continuous changes, this method is the only one available for production use as the refresh token will be frequently refreshed and this file MUST be writable by the library to keep a usable refresh token.
64-
65-
2. Some or all values can be overriden by environment variables. This is the easiest method if your are packaging your application with Docker. It also allow you to do some testing with other accounts without touching your current ~/.netatmo.credentials file
75+
3. Some or all values can be overriden by environment variables. This is the easiest method if your are packaging your application with Docker.
6676

77+
̀̀```bash
6778
$ export REFRESH_TOKEN="yyy"
6879
$ python3 MyCodeUsingLnetatmo.py
69-
...
80+
̀̀```
7081
71-
3. Some or all values can be overriden by explicit call to initializer of ClientAuth class
72-
73-
# Example: REFRESH_TOKEN supposed to be defined by one of the previous methods
74-
authData = lnetatmo.ClientAuth( clientId="netatmo-client-id",
75-
clientSecret="secret" )
82+
> Due to Netatmo continuous changes, the credential file is the recommended method for production use as the refresh token will be frequently refreshed and this file MUST be writable by the library to keep a usable refresh token. You can also recover the `authorization.refreshToken` before your program termination and save it to be able to pass it back when your program restart.
7683
77-
If you provide all the values, using any method or mix except 3, you can test that everything is working properly by simply running the package as a standalone program.
84+
If you provide all the values in a credential file, you can test that everything is working properly by simply running the package as a standalone program.
7885

7986
This will run a full access test to the account and stations and return 0 as return code if everything works well. If run interactively, it will also display an OK message.
8087

@@ -171,6 +178,7 @@ Constructor
171178
authorization = lnetatmo.ClientAuth( clientId = _CLIENT_ID,
172179
clientSecret = _CLIENT_SECRET,
173180
refreshToken = _REFRESH_TOKEN,
181+
credentialFile = "~/.netatmo.credentials"
174182
)
175183
```
176184

0 commit comments

Comments
 (0)