From e000d4b47482d5365bec299cecd2fea47d182178 Mon Sep 17 00:00:00 2001 From: Alexey Lebedeff Date: Sun, 22 Apr 2018 09:19:00 +0200 Subject: [PATCH 001/333] Prevent failure from uninitialized modules This can be seen e.g. when outdoor module is not connected. In that case API returns a stub data for that module, that contains neither 'module_name' nor 'dashboard_data'. --- smart_home/WeatherStation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 74e61fe9f..2e7ea0a75 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -27,6 +27,8 @@ def __init__(self, authData): self.modules = dict() for i in range(len(self.rawData)): for m in self.rawData[i]['modules']: + if 'module_name' not in m: + continue self.modules[ m['_id'] ] = m self.modules[ m['_id'] ][ 'main_device' ] = self.rawData[i]['_id'] self.default_station = list(self.stations.values())[0]['station_name'] @@ -106,6 +108,8 @@ def lastData(self, station=None, exclude=0): lastD[s['module_name']]['When'] = lastD[s['module_name']].pop("time_utc") lastD[s['module_name']]['wifi_status'] = s['wifi_status'] for module in s["modules"]: + if 'dashboard_data' not in module: + continue ds = module['dashboard_data'] if ds['time_utc'] > limit : lastD[module['module_name']] = ds.copy() From cc6e32ba9a6b0ad4c2d9618ec6c8955cf8d3d82e Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Mon, 4 Jun 2018 14:22:07 +0200 Subject: [PATCH 002/333] Pypi preparation This package is now named pyatmo to avoid conflict with lnetamo Signed-off-by: Hugo Dupras --- LICENSE.txt | 21 +++++++++++++++++++++ MANIFEST.in | 2 ++ lnetatmo.py => pyatmo.py | 0 setup.cfg | 2 ++ setup.py | 17 +++++++++-------- 5 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 LICENSE.txt create mode 100644 MANIFEST.in rename lnetatmo.py => pyatmo.py (100%) create mode 100644 setup.cfg diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..4742cd498 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Hugo DUPRAS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..3e677d057 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include README.md +include LICENSE.txt diff --git a/lnetatmo.py b/pyatmo.py similarity index 100% rename from lnetatmo.py rename to pyatmo.py diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..b88034e41 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py index a6e44754c..f4ba4bbb7 100755 --- a/setup.py +++ b/setup.py @@ -2,20 +2,21 @@ # python setup.py --dry-run --verbose install from distutils.core import setup +import setuptools setup( - name='lnetatmo', - version='0.9.2.1', # Should be updated with new versions - author='Philippe Larduinat', - author_email='philippelt@users.sourceforge.net', - py_modules=['lnetatmo'], + name='pyatmo', + version='1.0', # Should be updated with new versions + author='Hugo Dupras', + author_email='jabesq@gmail.com', + py_modules=['pyatmo'], packages=['smart_home'], package_dir={'smart_home': 'smart_home'}, scripts=[], data_files=[], - url='https://github.com/philippelt/netatmo-api-python', - license='Open Source', - description='Simple API to access Netatmo weather station data from any python script.', + url='https://github.com/jabesq/netatmo-api-python', + license='MIT', + description='Simple API to access Netatmo weather station data from any python script. Design for Home-Assitant (but not only)', long_description=open('README.md').read() ) From a3c330f14e1aedc64f4728f6dfd42e1313fc3287 Mon Sep 17 00:00:00 2001 From: Colin Frei Date: Fri, 6 Jul 2018 22:21:46 +0200 Subject: [PATCH 003/333] Add class for public data Most of this is copied from https://github.com/SebastienPittet/netatmo-api-python/blob/master/lnetatmo.py --- smart_home/PublicData.py | 90 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 smart_home/PublicData.py diff --git a/smart_home/PublicData.py b/smart_home/PublicData.py new file mode 100644 index 000000000..048704b4e --- /dev/null +++ b/smart_home/PublicData.py @@ -0,0 +1,90 @@ +""" +coding=utf-8 +""" +import warnings, time + +from . import NoDevice, postRequest, todayStamps, _BASE_URL + +_GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata" +_LON_NE = 6.221652 +_LAT_NE = 46.610870 +_LON_SW = 6.217828 +_LAT_SW = 46.596485 + +class PublicData: + + def __init__(self, + authData, + LAT_NE = _LAT_NE, + LON_NE = _LON_NE, + LAT_SW = _LAT_SW, + LON_SW=_LON_SW, + required_data_type = "rain", # "humidity" is the only 2nd choice + filtering=False): + self.getAuthToken = authData.accessToken + postParams = { + "access_token" : self.getAuthToken, + "lat_ne" : LAT_NE, + "lon_ne" : LON_NE, + "lat_sw" : LAT_SW, + "lon_sw" : LON_SW, + "required_data" : required_data_type, + "filter" : filtering + } + resp = postRequest(_GETPUBLIC_DATA, postParams) + self.rawData = resp['body'] + self.status = resp['status'] + self.time_exec = toTimeString(resp['time_exec']) + self.time_server = toTimeString(resp['time_server']) + + + def CountStationInArea(self): + return len(self.rawData) + + def get24h(self): + measures = {} # dict + for station in self.rawData: + for module in station['measures']: + for typeModule in station['measures'][module]: + if typeModule == 'rain_24h': + measures[station['_id']] = station['measures'][module]['rain_24h'] + return measures + + + def get60min(self): + measures = {} # dict + for station in self.rawData: + for module in station['measures']: + for typeModule in station['measures'][module]: + if typeModule == 'rain_60min': + measures[station['_id']] = station['measures'][module]['rain_60min'] + return measures + + def getLive(self): + measures = {} # dict + for station in self.rawData: + for module in station['measures']: + for typeModule in station['measures'][module]: + if typeModule == 'rain_live': + measures[station['_id']] = station['measures'][module]['rain_live'] + return measures + + def getLocations(self): + locations = {} #dict + for station in self.rawData: + locations [station['_id']] = station['place']['location'] + return locations + + def getTimeforMeasure(self): + measures_timestamps = {} # dict + for station in self.rawData: + for module in station['measures']: + for typeModule in station['measures'][module]: + if typeModule == 'rain_timeutc': + measures_timestamps[station['_id']] = station['measures'][module]['rain_timeutc'] + return measures_timestamps + + + +def toTimeString(value): + return time.strftime("%Y-%m-%d_%H:%M:%S", time.localtime(int(value))) From 93f734250b63abcaede87c820d76ff5b640e02c7 Mon Sep 17 00:00:00 2001 From: Colin Frei Date: Fri, 6 Jul 2018 22:23:52 +0200 Subject: [PATCH 004/333] Reference PublicData in pyatmo.py as well --- pyatmo.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyatmo.py b/pyatmo.py index 1ce999e34..353752592 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -16,6 +16,7 @@ from smart_home.WeatherStation import WeatherStationData, DeviceList from smart_home.Camera import CameraData from smart_home.Thermostat import ThermostatData +from smart_home.PublicData import PublicDataG from smart_home import _BASE_URL, postRequest, NoDevice ######################## USER SPECIFIC INFORMATION ###################### @@ -142,6 +143,9 @@ def __init__(self, authData): if stdout.isatty(): print("lnetatmo.py : warning, no thermostat available for testing") + + Public = PublicData(authorization) + # If we reach this line, all is OK # If launched interactively, display OK message From fd606976ed7bb7372d2ae54825f0b47296ffe093 Mon Sep 17 00:00:00 2001 From: Colin Frei Date: Fri, 6 Jul 2018 22:24:10 +0200 Subject: [PATCH 005/333] Update pyatmo.py --- pyatmo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyatmo.py b/pyatmo.py index 353752592..69c2eb21f 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -144,7 +144,7 @@ def __init__(self, authData): print("lnetatmo.py : warning, no thermostat available for testing") - Public = PublicData(authorization) + PublicData(authorization) # If we reach this line, all is OK From 2578d40b905763c370d56999b3ec5210dcb3f492 Mon Sep 17 00:00:00 2001 From: Colin Frei Date: Fri, 6 Jul 2018 22:29:54 +0200 Subject: [PATCH 006/333] Typo --- pyatmo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyatmo.py b/pyatmo.py index 69c2eb21f..b2f207837 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -16,7 +16,7 @@ from smart_home.WeatherStation import WeatherStationData, DeviceList from smart_home.Camera import CameraData from smart_home.Thermostat import ThermostatData -from smart_home.PublicData import PublicDataG +from smart_home.PublicData import PublicData from smart_home import _BASE_URL, postRequest, NoDevice ######################## USER SPECIFIC INFORMATION ###################### From 86cc542b9bf879235c3728ddb625edde50c3511a Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Tue, 10 Jul 2018 21:29:54 +0200 Subject: [PATCH 007/333] v1.1 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f4ba4bbb7..aa6bd22f9 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pyatmo', - version='1.0', # Should be updated with new versions + version='1.1', # Should be updated with new versions author='Hugo Dupras', author_email='jabesq@gmail.com', py_modules=['pyatmo'], From 80f92b80a05aeba81a775069ae10dc205605a849 Mon Sep 17 00:00:00 2001 From: Uladzimir Kazakevich Date: Mon, 16 Jul 2018 23:59:48 +0300 Subject: [PATCH 008/333] Fix 'modules' error in case there is only main station module configured --- smart_home/WeatherStation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 2e7ea0a75..6b3fec90d 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -26,6 +26,8 @@ def __init__(self, authData): self.stations = { d['_id'] : d for d in self.rawData } self.modules = dict() for i in range(len(self.rawData)): + if 'modules' not in self.rawData[i]: + self.rawData[i]['modules'] = [ self.rawData[i] ] for m in self.rawData[i]['modules']: if 'module_name' not in m: continue @@ -190,4 +192,4 @@ class DeviceList(WeatherStationData): """ warnings.warn("The 'DeviceList' class was renamed 'WeatherStationData'", DeprecationWarning ) - pass \ No newline at end of file + pass From ed74596842c8a04b21555bb1306221801d921a69 Mon Sep 17 00:00:00 2001 From: Giel Janssens Date: Tue, 19 Jun 2018 18:19:04 +0200 Subject: [PATCH 009/333] get even --- pyatmo.py | 3 --- smart_home/WeatherStation.py | 1 - 2 files changed, 4 deletions(-) diff --git a/pyatmo.py b/pyatmo.py index b2f207837..ae0ed274e 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -7,7 +7,6 @@ cameras or/and the Netatmo smart thermostat This package can be used with Python2 or Python3 applications and do not require anything else than standard libraries - PythonAPI Netatmo REST data access coding=utf-8 """ @@ -39,7 +38,6 @@ class ClientAuth: """ Request authentication and keep access token available through token method. Renew it automatically if necessary - Args: clientId (str): Application clientId delivered by Netatmo on dev.netatmo.com clientSecret (str): Application Secret key delivered by Netatmo on dev.netatmo.com @@ -97,7 +95,6 @@ def accessToken(self): class User: """ This class returns basic information about the user - Args: authData (ClientAuth): Authentication information with a working access Token """ diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 6b3fec90d..0a56faf4c 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -11,7 +11,6 @@ class WeatherStationData: """ List the Weather Station devices (stations and modules) - Args: authData (ClientAuth): Authentication information with a working access Token """ From 98ffa3e22051f074931a0383b2609c843328d694 Mon Sep 17 00:00:00 2001 From: Giel Janssens Date: Tue, 19 Jun 2018 18:22:43 +0200 Subject: [PATCH 010/333] fix for https://github.com/home-assistant/home-assistant/issues/15047 --- smart_home/Camera.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index 75359557a..c08343e28 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -47,18 +47,19 @@ def __init__(self, authData, size=15): self.types[nameHome] = dict() for p in self.rawData['homes'][i]['persons']: self.persons[p['id']] = p - for e in self.rawData['homes'][i]['events']: - if e['type'] == 'outdoor': - if e['camera_id'] not in self.outdoor_events: - self.outdoor_events[e['camera_id']] = dict() - self.outdoor_events[e['camera_id']][e['time']] = e - elif e['type'] != 'outdoor': - if e['camera_id'] not in self.events: - self.events[e['camera_id']] = dict() - self.events[e['camera_id']][e['time']] = e + if 'events' in self.rawData['homes'][i]: + for e in self.rawData['homes'][i]['events']: + if e['type'] == 'outdoor': + if e['camera_id'] not in self.outdoor_events: + self.outdoor_events[e['camera_id']] = dict() + self.outdoor_events[e['camera_id']][e['time']] = e + elif e['type'] != 'outdoor': + if e['camera_id'] not in self.events: + self.events[e['camera_id']] = dict() + self.events[e['camera_id']][e['time']] = e for c in self.rawData['homes'][i]['cameras']: self.cameras[nameHome][c['id']] = c - if c['type'] == 'NACamera' and 'modules' in c : + if c['type'] == 'NACamera' and 'modules' in c: for m in c['modules']: self.modules[m['id']] = m self.modules[m['id']]['cam_id'] = c['id'] @@ -300,7 +301,7 @@ def _knownPersons(self): def knownPersonsNames(self): names = [] - for p_id,p in self._knownPersons().items(): + for p_id, p in self._knownPersons().items(): names.append(p['pseudo']) return names @@ -474,7 +475,7 @@ def moduleMotionDetected(self, module=None, home=None, return True elif (self.lastEvent[cam_id]['type'] == 'tag_big_move' or self.lastEvent[cam_id]['type'] == 'tag_small_move') and\ - self.lastEvent[cam_id]['module_id'] == mod_id: + self.lastEvent[cam_id]['module_id'] == mod_id: return True return False @@ -498,9 +499,9 @@ def moduleOpened(self, module=None, home=None, camera=None, exclude=0): if time_ev < limit: return False elif self.events[cam_id][time_ev]['type'] == 'tag_open' and\ - self.events[cam_id][time_ev]['module_id'] == mod_id: + self.events[cam_id][time_ev]['module_id'] == mod_id: return True elif self.lastEvent[cam_id]['type'] == 'tag_open' and\ - self.lastEvent[cam_id]['module_id'] == mod_id: + self.lastEvent[cam_id]['module_id'] == mod_id: return True return False From 5fe08a7bead4cafd64fa01880aadff7ab9cdc467 Mon Sep 17 00:00:00 2001 From: Giel Janssens Date: Tue, 19 Jun 2018 18:25:53 +0200 Subject: [PATCH 011/333] fix for outdoor_motion keyError --- smart_home/Camera.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index c08343e28..e7326e469 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -391,10 +391,11 @@ def outdoormotionDetected(self, home=None, camera=None, offset=0): except TypeError: print("outdoormotionDetected: Camera name or home is unknown") return False - if self.lastEvent[cam_id]['type'] == 'movement': - if self.lastEvent[cam_id]['video_status'] == 'recording' and\ - self.lastEvent[cam_id]['time'] + offset > int(time.time()): - return True + if cam_id in self.lastEvent: + if self.lastEvent[cam_id]['type'] == 'movement': + if self.lastEvent[cam_id]['video_status'] == 'recording' and\ + self.lastEvent[cam_id]['time'] + offset > int(time.time()): + return True return False def humanDetected(self, home=None, camera=None, offset=0): From c88fb9447fc1a0fb11f298f70d18635f98339f33 Mon Sep 17 00:00:00 2001 From: Giel Janssens Date: Wed, 11 Jul 2018 11:00:26 +0200 Subject: [PATCH 012/333] fix for self.default_camera? --- smart_home/Camera.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index e7326e469..1999ec209 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -48,6 +48,7 @@ def __init__(self, authData, size=15): for p in self.rawData['homes'][i]['persons']: self.persons[p['id']] = p if 'events' in self.rawData['homes'][i]: + self.default_home = self.rawData['homes'][i]['name'] for e in self.rawData['homes'][i]['events']: if e['type'] == 'outdoor': if e['camera_id'] not in self.outdoor_events: @@ -71,7 +72,7 @@ def __init__(self, authData, size=15): for camera in self.outdoor_events: self.outdoor_lastEvent[camera] = self.outdoor_events[camera][ sorted(self.outdoor_events[camera])[-1]] - self.default_home = list(self.homes.values())[0]['name'] + # self.default_home = list(self.homes.values())[0]['name'] if self.modules != {}: self.default_module = list(self.modules.values())[0]['name'] else: From 9c55a2b84c5f205a461efe405d8c84d7bed3287a Mon Sep 17 00:00:00 2001 From: Giel Janssens Date: Wed, 11 Jul 2018 12:47:50 +0200 Subject: [PATCH 013/333] del # --- smart_home/Camera.py | 1 - 1 file changed, 1 deletion(-) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index 1999ec209..a35200d03 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -72,7 +72,6 @@ def __init__(self, authData, size=15): for camera in self.outdoor_events: self.outdoor_lastEvent[camera] = self.outdoor_events[camera][ sorted(self.outdoor_events[camera])[-1]] - # self.default_home = list(self.homes.values())[0]['name'] if self.modules != {}: self.default_module = list(self.modules.values())[0]['name'] else: From 7860d5f77313f46235e80c8edeff642453458b5b Mon Sep 17 00:00:00 2001 From: Giel Janssens Date: Mon, 16 Jul 2018 15:12:49 +0200 Subject: [PATCH 014/333] remove whitespace --- smart_home/Camera.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index a35200d03..a7c6868c5 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -476,7 +476,7 @@ def moduleMotionDetected(self, module=None, home=None, return True elif (self.lastEvent[cam_id]['type'] == 'tag_big_move' or self.lastEvent[cam_id]['type'] == 'tag_small_move') and\ - self.lastEvent[cam_id]['module_id'] == mod_id: + self.lastEvent[cam_id]['module_id'] == mod_id: return True return False @@ -500,9 +500,9 @@ def moduleOpened(self, module=None, home=None, camera=None, exclude=0): if time_ev < limit: return False elif self.events[cam_id][time_ev]['type'] == 'tag_open' and\ - self.events[cam_id][time_ev]['module_id'] == mod_id: + self.events[cam_id][time_ev]['module_id'] == mod_id: return True elif self.lastEvent[cam_id]['type'] == 'tag_open' and\ - self.lastEvent[cam_id]['module_id'] == mod_id: + self.lastEvent[cam_id]['module_id'] == mod_id: return True return False From 4e4b8afe98be6fa7ed37cd8506773ef62b700baa Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Tue, 17 Jul 2018 10:16:25 +0200 Subject: [PATCH 015/333] v1.1.1 Minor fixes for Camera and Station modules Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index aa6bd22f9..06b831854 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pyatmo', - version='1.1', # Should be updated with new versions + version='1.1.1', # Should be updated with new versions author='Hugo Dupras', author_email='jabesq@gmail.com', py_modules=['pyatmo'], From 8b1d3cf8392fb73803b228a602b5351ac4c8fcbd Mon Sep 17 00:00:00 2001 From: GP8x Date: Fri, 7 Sep 2018 23:07:35 +0100 Subject: [PATCH 016/333] Add support for the full array of GetPublicData measures --- smart_home/PublicData.py | 190 +++++++++++++++++++++++++++------------ 1 file changed, 135 insertions(+), 55 deletions(-) diff --git a/smart_home/PublicData.py b/smart_home/PublicData.py index 048704b4e..f36c9357d 100644 --- a/smart_home/PublicData.py +++ b/smart_home/PublicData.py @@ -1,9 +1,9 @@ """ coding=utf-8 """ -import warnings, time +import time -from . import NoDevice, postRequest, todayStamps, _BASE_URL +from . import postRequest, _BASE_URL _GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata" _LON_NE = 6.221652 @@ -11,80 +11,160 @@ _LON_SW = 6.217828 _LAT_SW = 46.596485 +_STATION_TEMPERATURE_TYPE = "temperature" +_STATION_PRESSURE_TYPE = "pressure" +_STATION_HUMIDITY_TYPE = "humidity" + +_ACCESSORY_RAIN_LIVE_TYPE = "rain_live" +_ACCESSORY_RAIN_60MIN_TYPE = "rain_60min" +_ACCESSORY_RAIN_24H_TYPE = "rain_24h" +_ACCESSORY_RAIN_TIME_TYPE = "rain_timeutc" +_ACCESSORY_WIND_STRENGTH_TYPE = "wind_strength" +_ACCESSORY_WIND_ANGLE_TYPE = "wind_angle" +_ACCESSORY_WIND_TIME_TYPE = "wind_timeutc" +_ACCESSORY_GUST_STRENGTH_TYPE = "gust_strength" +_ACCESSORY_GUST_ANGLE_TYPE = "gust_angle" + + class PublicData: def __init__(self, - authData, - LAT_NE = _LAT_NE, - LON_NE = _LON_NE, - LAT_SW = _LAT_SW, - LON_SW=_LON_SW, - required_data_type = "rain", # "humidity" is the only 2nd choice - filtering=False): - self.getAuthToken = authData.accessToken - postParams = { - "access_token" : self.getAuthToken, - "lat_ne" : LAT_NE, - "lon_ne" : LON_NE, - "lat_sw" : LAT_SW, - "lon_sw" : LON_SW, - "required_data" : required_data_type, - "filter" : filtering + auth_data, + LAT_NE=_LAT_NE, + LON_NE=_LON_NE, + LAT_SW=_LAT_SW, + LON_SW=_LON_SW, + required_data_type=None, # comma-separated list from above _STATION or _ACCESSORY values + filtering=False): + self.getAuthToken = auth_data.accessToken + post_params = { + "access_token": self.getAuthToken, + "lat_ne": LAT_NE, + "lon_ne": LON_NE, + "lat_sw": LAT_SW, + "lon_sw": LON_SW, + "filter": filtering } - resp = postRequest(_GETPUBLIC_DATA, postParams) - self.rawData = resp['body'] + + if required_data_type: + post_params['required_data'] = required_data_type + + resp = postRequest(_GETPUBLIC_DATA, post_params) + self.raw_data = resp['body'] self.status = resp['status'] self.time_exec = toTimeString(resp['time_exec']) self.time_server = toTimeString(resp['time_server']) - def CountStationInArea(self): - return len(self.rawData) + return len(self.raw_data) - def get24h(self): - measures = {} # dict - for station in self.rawData: - for module in station['measures']: - for typeModule in station['measures'][module]: - if typeModule == 'rain_24h': - measures[station['_id']] = station['measures'][module]['rain_24h'] - return measures + # Backwards compatibility for < 1.2 + def getLive(self): + return self.getLatestRain() + + def getLatestRain(self): + return self.getAccessoryMeasures(_ACCESSORY_RAIN_LIVE_TYPE) + def getAverageRain(self): + return averageMeasure(self.getLatestRain()) + # Backwards compatibility for < 1.2 def get60min(self): - measures = {} # dict - for station in self.rawData: - for module in station['measures']: - for typeModule in station['measures'][module]: - if typeModule == 'rain_60min': - measures[station['_id']] = station['measures'][module]['rain_60min'] - return measures + return self.get60minRain() - def getLive(self): - measures = {} # dict - for station in self.rawData: - for module in station['measures']: - for typeModule in station['measures'][module]: - if typeModule == 'rain_live': - measures[station['_id']] = station['measures'][module]['rain_live'] - return measures + def get60minRain(self): + return self.getAccessoryMeasures(_ACCESSORY_RAIN_60MIN_TYPE) + + def getAverage60minRain(self): + return averageMeasure(self.get60minRain()) + + # Backwards compatibility for < 1.2 + def get24h(self): + return self.get24hRain() + + def get24hRain(self): + return self.getAccessoryMeasures(_ACCESSORY_RAIN_24H_TYPE) + + def getAverage24hRain(self): + return averageMeasure(self.get24hRain()) + + def getLatestPressures(self): + return self.getLatestStationMeasures(_STATION_PRESSURE_TYPE) + + def getAveragePressure(self): + return averageMeasure(self.getLatestPressures()) + + def getLatestTemperatures(self): + return self.getLatestStationMeasures(_STATION_TEMPERATURE_TYPE) + + def getAverageTemperature(self): + return averageMeasure(self.getLatestTemperatures()) + + def getLatestHumidities(self): + return self.getLatestStationMeasures(_STATION_HUMIDITY_TYPE) + + def getAverageHumidity(self): + return averageMeasure(self.getLatestHumidities()) + + def getLatestWindStrengths(self): + return self.getAccessoryMeasures(_ACCESSORY_WIND_STRENGTH_TYPE) + + def getAverageWindStrength(self): + return averageMeasure(self.getLatestWindStrengths()) + + def getLatestWindAngles(self): + return self.getAccessoryMeasures(_ACCESSORY_WIND_ANGLE_TYPE) + + def getLatestGustStrengths(self): + return self.getAccessoryMeasures(_ACCESSORY_GUST_STRENGTH_TYPE) + + def getAverageGustStrength(self): + return averageMeasure(self.getLatestGustStrengths()) + + def getLatestGustAngles(self): + return self.getAccessoryMeasures(_ACCESSORY_GUST_ANGLE_TYPE) def getLocations(self): - locations = {} #dict - for station in self.rawData: - locations [station['_id']] = station['place']['location'] + locations = {} + for station in self.raw_data: + locations[station['_id']] = station['place']['location'] return locations + # Backwards compatibility for < 1.2 def getTimeforMeasure(self): - measures_timestamps = {} # dict - for station in self.rawData: - for module in station['measures']: - for typeModule in station['measures'][module]: - if typeModule == 'rain_timeutc': - measures_timestamps[station['_id']] = station['measures'][module]['rain_timeutc'] - return measures_timestamps + return self.getTimeForRainMeasures() + def getTimeForRainMeasures(self): + return self.getAccessoryMeasures(_ACCESSORY_RAIN_TIME_TYPE) + + def getTimeForWindMeasures(self): + return self.getAccessoryMeasures(_ACCESSORY_WIND_TIME_TYPE) + + def getLatestStationMeasures(self, type): + measures = {} + for station in self.raw_data: + for _, module in station['measures'].items(): + if 'type' in module and type in module['type'] and 'res' in module and module['res']: + measure_index = module['type'].index(type) + latest_timestamp = sorted(module['res'], reverse=True)[0] + measures[station['_id']] = module['res'][latest_timestamp][measure_index] + return measures + + def getAccessoryMeasures(self, type): + measures = {} + for station in self.raw_data: + for _, module in station['measures'].items(): + if type in module: + measures[station['_id']] = module[type] + return measures def toTimeString(value): return time.strftime("%Y-%m-%d_%H:%M:%S", time.localtime(int(value))) + + +def averageMeasure(measures): + if measures: + return sum(measures.values()) / len(measures) + else: + return 0.0 From c6be4fe356c5bf7f9710ad1605086fcbffd12672 Mon Sep 17 00:00:00 2001 From: GP8x Date: Fri, 7 Sep 2018 23:32:38 +0100 Subject: [PATCH 017/333] v1.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 06b831854..938211467 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pyatmo', - version='1.1.1', # Should be updated with new versions + version='1.2', # Should be updated with new versions author='Hugo Dupras', author_email='jabesq@gmail.com', py_modules=['pyatmo'], From 3e6e04c8877caba953bab4f24d152f41738f87be Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Tue, 25 Sep 2018 07:14:07 -0400 Subject: [PATCH 018/333] Add support for battery_percent --- smart_home/WeatherStation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 0a56faf4c..909e47834 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -93,8 +93,8 @@ def monitoredConditions(self, module): # the main module has wifi_status conditions.append('wifi_status') else: - # assume all other modules have rf_status and battery_vp - conditions.extend(['rf_status', 'battery_vp']) + # assume all other modules have rf_status, battery_vp, and battery_percent + conditions.extend(['rf_status', 'battery_vp', 'battery_percent']) return conditions def lastData(self, station=None, exclude=0): @@ -116,7 +116,7 @@ def lastData(self, station=None, exclude=0): lastD[module['module_name']] = ds.copy() lastD[module['module_name']]['When'] = lastD[module['module_name']].pop("time_utc") # For potential use, add battery and radio coverage information to module data if present - for i in ('battery_vp', 'rf_status') : + for i in ('rf_status', 'battery_vp', 'battery_percent') : if i in module : lastD[module['module_name']][i] = module[i] return lastD From 80a683dabca74948202fdd8fe93323ff67983092 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Mon, 1 Oct 2018 21:13:53 +0200 Subject: [PATCH 019/333] v1.2.1 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 938211467..6e619c697 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pyatmo', - version='1.2', # Should be updated with new versions + version='1.2.1', # Should be updated with new versions author='Hugo Dupras', author_email='jabesq@gmail.com', py_modules=['pyatmo'], From d9604b63d0237d147cfa0d8b50e0b5bc7607478b Mon Sep 17 00:00:00 2001 From: Ante Vukorepa Date: Sun, 4 Nov 2018 20:13:12 +0100 Subject: [PATCH 020/333] Fix wrong setpoint read from endpoint in manual setpoint mode Netatmo, in their infinite wisdom, decided to use two separate properties for the setpoint temperature in the Getthermostatdata endpoint - one for the manual mode, and one for everything else. The library incorrectly assumes that the setpoint temperature in manual mode should be read from the same property as for other modes. This commit should fix it. --- smart_home/Thermostat.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index db260fb1c..ce282bea7 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -29,8 +29,11 @@ def __init__(self, authData): self.modList = self.devList[0]['modules'] self.modId = self.modList[0]['_id'] self.temp = self.modList[0]['measured']['temperature'] - self.setpoint_temp = self.modList[0]['measured']['setpoint_temp'] self.setpoint_mode = self.modList[0]['setpoint']['setpoint_mode'] + if self.setpoint_mode == 'manual': + self.setpoint_temp = self.modList[0]['setpoint']['setpoint_temp'] + else: + self.setpoint_temp = self.modList[0]['measured']['setpoint_temp'] self.relay_cmd = int(self.modList[0]['therm_relay_cmd']) self.devices = { d['_id'] : d for d in self.rawData['devices'] } self.modules = dict() From afe81c4a89b074aa92799292861abc8c45d1b45a Mon Sep 17 00:00:00 2001 From: Luis Martinez de Bartolome Date: Sun, 4 Nov 2018 11:33:59 +0100 Subject: [PATCH 021/333] Add support for HomeCoach --- smart_home/HomeCoach.py | 18 ++++++++++++++++++ smart_home/WeatherStation.py | 5 +++-- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 smart_home/HomeCoach.py diff --git a/smart_home/HomeCoach.py b/smart_home/HomeCoach.py new file mode 100644 index 000000000..07d345ef9 --- /dev/null +++ b/smart_home/HomeCoach.py @@ -0,0 +1,18 @@ +""" +coding=utf-8 +""" +from .WeatherStation import WeatherStationData + +from . import _BASE_URL + +_GETHOMECOACHDATA_REQ = _BASE_URL + "api/gethomecoachsdata" + + +class HomeCoachData(WeatherStationData): + """ + List the Home Couch devices (stations and modules) + Args: + authData (ClientAuth): Authentication information with a working access Token + """ + def __init__(self, authData): + super(HomeCoachData, self).__init__(authData, urlReq=_GETHOMECOACHDATA_REQ) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 909e47834..53190efc3 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -14,12 +14,13 @@ class WeatherStationData: Args: authData (ClientAuth): Authentication information with a working access Token """ - def __init__(self, authData): + def __init__(self, authData, urlReq=None): + self.urlReq = urlReq or _GETMEASURE_REQ self.getAuthToken = authData.accessToken postParams = { "access_token" : self.getAuthToken } - resp = postRequest(_GETSTATIONDATA_REQ, postParams) + resp = postRequest(self.urlReq, postParams) self.rawData = resp['body']['devices'] if not self.rawData : raise NoDevice("No weather station available") self.stations = { d['_id'] : d for d in self.rawData } From fda00a4f0b5b8e83946423ce5efa63bbf0a0d47f Mon Sep 17 00:00:00 2001 From: Luis Martinez de Bartolome Date: Sun, 4 Nov 2018 12:29:02 +0100 Subject: [PATCH 022/333] Global import --- pyatmo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyatmo.py b/pyatmo.py index ae0ed274e..bb7ed00b6 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -16,6 +16,7 @@ from smart_home.Camera import CameraData from smart_home.Thermostat import ThermostatData from smart_home.PublicData import PublicData +from smart_home.HomeCoach import HomeCoachData from smart_home import _BASE_URL, postRequest, NoDevice ######################## USER SPECIFIC INFORMATION ###################### From cf5d4abd315eb0c70fbdfafa6e95d061fb9f125e Mon Sep 17 00:00:00 2001 From: Luis Martinez de Bartolome Date: Sun, 4 Nov 2018 13:36:47 +0100 Subject: [PATCH 023/333] Avoid duplicated module names --- smart_home/WeatherStation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 53190efc3..32561d250 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -18,7 +18,7 @@ def __init__(self, authData, urlReq=None): self.urlReq = urlReq or _GETMEASURE_REQ self.getAuthToken = authData.accessToken postParams = { - "access_token" : self.getAuthToken + "access_token": self.getAuthToken } resp = postRequest(self.urlReq, postParams) self.rawData = resp['body']['devices'] @@ -36,13 +36,13 @@ def __init__(self, authData, urlReq=None): self.default_station = list(self.stations.values())[0]['station_name'] def modulesNamesList(self, station=None): - res = [m['module_name'] for m in self.modules.values()] + res = set([m['module_name'] for m in self.modules.values()]) if station: - res.append(self.stationByName(station)['module_name']) + res.add(self.stationByName(station)['module_name']) else: for id,station in self.stations.items(): - res.append(station['module_name']) - return res + res.add(station['module_name']) + return list(res) def stationByName(self, station=None): if not station : station = self.default_station From fe075e8dc4b6560898b1c6cbe1c75a4844320505 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Tue, 6 Nov 2018 20:44:20 +0100 Subject: [PATCH 024/333] Better error handling when failing to get data Signed-off-by: Hugo D. (jabesq) --- smart_home/Camera.py | 2 ++ smart_home/WeatherStation.py | 4 +++- smart_home/__init__.py | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index a7c6868c5..8363dc0e1 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -27,6 +27,8 @@ def __init__(self, authData, size=15): "size": size } resp = postRequest(_GETHOMEDATA_REQ, postParams) + if 'body' not in resp: + raise URLError('No data returned by Netatmo server') self.rawData = resp['body'] self.homes = {d['id']: d for d in self.rawData['homes']} if not self.homes: diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 32561d250..2ab6f0438 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -113,12 +113,14 @@ def lastData(self, station=None, exclude=0): if 'dashboard_data' not in module: continue ds = module['dashboard_data'] - if ds['time_utc'] > limit : + if 'time_utc' in ds and ds['time_utc'] > limit : lastD[module['module_name']] = ds.copy() lastD[module['module_name']]['When'] = lastD[module['module_name']].pop("time_utc") # For potential use, add battery and radio coverage information to module data if present for i in ('rf_status', 'battery_vp', 'battery_percent') : if i in module : lastD[module['module_name']][i] = module[i] + else: + return None return lastD def checkNotUpdated(self, station=None, delay=3600): diff --git a/smart_home/__init__.py b/smart_home/__init__.py index 612f82164..ed91b8117 100644 --- a/smart_home/__init__.py +++ b/smart_home/__init__.py @@ -1,3 +1,5 @@ +import time +import traceback from sys import version_info import json @@ -27,6 +29,7 @@ def postRequest(url, params=None, timeout=10): try: resp = urllib.request.urlopen(req, params, timeout=timeout) if params else urllib.request.urlopen(req, timeout=timeout) except urllib.error.URLError: + print(traceback.format_exc()) return None else: if params: @@ -61,6 +64,8 @@ def todayStamps(): def getStationMinMaxTH(station=None, module=None): + from pyatmo import ClientAuth + from smart_home.WeatherStation import DeviceList authorization = ClientAuth() devList = DeviceList(authorization) if not station : station = devList.default_station From c45e62c74c8f2a8cfee1925ee50f5446cf062acb Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Tue, 6 Nov 2018 20:46:21 +0100 Subject: [PATCH 025/333] v1.3 Signed-off-by: Hugo D. (jabesq) --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 6e619c697..8f816f8bc 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pyatmo', - version='1.2.1', # Should be updated with new versions + version='1.3', # Should be updated with new versions author='Hugo Dupras', author_email='jabesq@gmail.com', py_modules=['pyatmo'], @@ -17,6 +17,7 @@ data_files=[], url='https://github.com/jabesq/netatmo-api-python', license='MIT', - description='Simple API to access Netatmo weather station data from any python script. Design for Home-Assitant (but not only)', + description='Simple API to access Netatmo weather station data from any python script. ' + 'Design for Home-Assitant (but not only)', long_description=open('README.md').read() ) From aedcf864d35e6babfaeb9961e6a87af665cc3b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 29 Nov 2018 12:49:28 +0100 Subject: [PATCH 026/333] Revert error from #18 --- smart_home/WeatherStation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 2ab6f0438..1fac63214 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -15,7 +15,7 @@ class WeatherStationData: authData (ClientAuth): Authentication information with a working access Token """ def __init__(self, authData, urlReq=None): - self.urlReq = urlReq or _GETMEASURE_REQ + self.urlReq = urlReq or _GETSTATIONDATA_REQ self.getAuthToken = authData.accessToken postParams = { "access_token": self.getAuthToken From 1dc0b2652d2fb9512f11e213e0a54382d0c4340e Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Thu, 29 Nov 2018 22:49:17 +0100 Subject: [PATCH 027/333] v1.4 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8f816f8bc..94116e9dd 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pyatmo', - version='1.3', # Should be updated with new versions + version='1.4', # Should be updated with new versions author='Hugo Dupras', author_email='jabesq@gmail.com', py_modules=['pyatmo'], From 79704a06b9fc650958d1623806a9759b036d499a Mon Sep 17 00:00:00 2001 From: Harald Kleppe Date: Thu, 29 Nov 2018 21:51:20 +0000 Subject: [PATCH 028/333] Make module loading more robust In my environment, homeassistant won't consistently load netatmo without this patch applied The following exceptions appear in the home assistant log without this patch: Nov 29 20:19:11 hasspi hass[324]: 2018-11-29 20:19:11 ERROR (MainThread) [homeassistant.components.sensor] Error while setting up platform netatmo Nov 29 20:19:11 hasspi hass[324]: Traceback (most recent call last): Nov 29 20:19:11 hasspi hass[324]: File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/helpers/entity_platform.py", line 128, in _async_setup_platform Nov 29 20:19:11 hasspi hass[324]: SLOW_SETUP_MAX_WAIT, loop=hass.loop) Nov 29 20:19:11 hasspi hass[324]: File "/usr/lib/python3.5/asyncio/tasks.py", line 400, in wait_for Nov 29 20:19:11 hasspi hass[324]: return fut.result() Nov 29 20:19:11 hasspi hass[324]: File "/usr/lib/python3.5/asyncio/futures.py", line 293, in result Nov 29 20:19:11 hasspi hass[324]: raise self._exception Nov 29 20:19:11 hasspi hass[324]: File "/usr/lib/python3.5/concurrent/futures/thread.py", line 55, in run Nov 29 20:19:11 hasspi hass[324]: result = self.fn(*self.args, **self.kwargs) Nov 29 20:19:11 hasspi hass[324]: File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/components/sensor/netatmo.py", line 85, in setup_platform Nov 29 20:19:11 hasspi hass[324]: dev.append(NetAtmoSensor(data, module_name, variable)) Nov 29 20:19:11 hasspi hass[324]: File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/components/sensor/netatmo.py", line 116, in __init__ Nov 29 20:19:11 hasspi hass[324]: station_data.moduleByName(module=module_name)['_id'] Nov 29 20:19:11 hasspi hass[324]: File "/srv/homeassistant/lib/python3.5/site-packages/smart_home/WeatherStation.py", line 65, in moduleByName Nov 29 20:19:11 hasspi hass[324]: if station['module_name'] == module: Nov 29 20:19:11 hasspi hass[324]: KeyError: 'module_name' ... Nov 29 20:19:15 hasspi hass[324]: Traceback (most recent call last): Nov 29 20:19:15 hasspi hass[324]: File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/helpers/entity_platform.py", line 128, in _async_setup_platform Nov 29 20:19:15 hasspi hass[324]: SLOW_SETUP_MAX_WAIT, loop=hass.loop) Nov 29 20:19:15 hasspi hass[324]: File "/usr/lib/python3.5/asyncio/tasks.py", line 400, in wait_for Nov 29 20:19:15 hasspi hass[324]: return fut.result() Nov 29 20:19:15 hasspi hass[324]: File "/usr/lib/python3.5/asyncio/futures.py", line 293, in result Nov 29 20:19:15 hasspi hass[324]: raise self._exception Nov 29 20:19:15 hasspi hass[324]: File "/usr/lib/python3.5/concurrent/futures/thread.py", line 55, in run Nov 29 20:19:15 hasspi hass[324]: result = self.fn(*self.args, **self.kwargs) Nov 29 20:19:15 hasspi hass[324]: File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/components/sensor/netatmo.py", line 87, in setup_platform Nov 29 20:19:15 hasspi hass[324]: for module_name in data.get_module_names(): Nov 29 20:19:15 hasspi hass[324]: File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/components/sensor/netatmo.py", line 304, in get_module_names Nov 29 20:19:15 hasspi hass[324]: self.update() Nov 29 20:19:15 hasspi hass[324]: File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/components/sensor/netatmo.py", line 330, in update Nov 29 20:19:15 hasspi hass[324]: self.data = self.station_data.lastData(exclude=3600) Nov 29 20:19:15 hasspi hass[324]: File "/srv/homeassistant/lib/python3.5/site-packages/smart_home/WeatherStation.py", line 108, in lastData Nov 29 20:19:15 hasspi hass[324]: lastD[s['module_name']] = ds.copy() Nov 29 20:19:15 hasspi hass[324]: KeyError: 'module_name' --- smart_home/WeatherStation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 1fac63214..fbbbcc2bb 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -63,7 +63,7 @@ def moduleByName(self, module, station=None): return s else: for id, station in self.stations.items(): - if station['module_name'] == module: + if 'module_name' in station and station['module_name'] == module: return station for m in self.modules: mod = self.modules[m] @@ -105,7 +105,7 @@ def lastData(self, station=None, exclude=0): # Define oldest acceptable sensor measure event limit = (time.time() - exclude) if exclude else 0 ds = s['dashboard_data'] - if ds['time_utc'] > limit : + if 'module_name' in s and ds['time_utc'] > limit : lastD[s['module_name']] = ds.copy() lastD[s['module_name']]['When'] = lastD[s['module_name']].pop("time_utc") lastD[s['module_name']]['wifi_status'] = s['wifi_status'] From 90dd200353c1425d4b3a940b9c04996abfe57dd5 Mon Sep 17 00:00:00 2001 From: Baosong Shan Date: Mon, 3 Dec 2018 11:29:39 +0100 Subject: [PATCH 029/333] Added two new classes in smart_home.Thermostat, to support multiple thermostats/valves --- pyatmo.py | 2 +- smart_home/Thermostat.py | 282 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+), 1 deletion(-) diff --git a/pyatmo.py b/pyatmo.py index bb7ed00b6..f052beb04 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -14,7 +14,7 @@ from smart_home.WeatherStation import WeatherStationData, DeviceList from smart_home.Camera import CameraData -from smart_home.Thermostat import ThermostatData +from smart_home.Thermostat import ThermostatData, HomeData, HomeStatus from smart_home.PublicData import PublicData from smart_home.HomeCoach import HomeCoachData from smart_home import _BASE_URL, postRequest, NoDevice diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index ce282bea7..cf8145c36 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -7,6 +7,288 @@ _SETTEMP_REQ = _BASE_URL + "api/setthermpoint" _GETTHERMOSTATDATA_REQ = _BASE_URL + "api/getthermostatsdata" +_GETHOMESDATA_REQ = _BASE_URL + "api/homesdata" +_GETHOMESTATUS_REQ = _BASE_URL + "api/homestatus" +_SETTHERMMODE_REQ = _BASE_URL + "api/setthermmode" +_SETROOMTHERMPOINT_REQ = _BASE_URL + "api/setroomthermpoint" +_GETROOMMEASURE_REQ = _BASE_URL + "api/getroommeasure" + + +class HomeData: + """ + List the Energy devices (relays, thermostat modules and valves) + + Args: + authData (ClientAuth): Authentication information with a working access Token + """ + def __init__(self, authData): + self.getAuthToken = authData.accessToken + postParams = { + "access_token": self.getAuthToken + } + resp = postRequest(_GETHOMESDATA_REQ, postParams) + + self.rawData = resp['body']['homes'] + self.homes = {d['id']: d for d in self.rawData} + if not self.homes: + raise NoDevice("No thermostat available") + self.modules = dict() + self.rooms = dict() + self.schedules = dict() + self.zones = dict() + self.setpoint_duration = dict() + for i in range(len(self.rawData)): + nameHome = self.rawData[i]['name'] + if 'modules' in self.rawData[i]: + if nameHome not in self.modules: + self.modules[nameHome] = dict() + self.default_home = self.rawData[i]['name'] + for m in self.rawData[i]['modules']: + self.modules[nameHome][m['id']] = m + if nameHome not in self.rooms: + self.rooms[nameHome] = dict() + if nameHome not in self.schedules: + self.schedules[nameHome] = dict() + if nameHome not in self.zones: + self.zones[nameHome] = dict() + if nameHome not in self.setpoint_duration: + self.setpoint_duration[nameHome] = dict() + if 'therm_setpoint_default_duration' in self.rawData[i]: + self.setpoint_duration[nameHome] = self.rawData[i]['therm_setpoint_default_duration'] + if 'rooms' in self.rawData[i]: + for r in self.rawData[i]['rooms']: + self.rooms[nameHome][r['id']] = r + if 'therm_schedules' in self.rawData[i]: + for s in self.rawData[i]['therm_schedules']: + self.schedules[nameHome][s['id']] = s + for t in range(len(self.rawData[i]['therm_schedules'])): + nameSchedule = self.rawData[i]['therm_schedules'][t]['name'] + if nameSchedule not in self.zones[nameHome]: + self.zones[nameHome][nameSchedule] = dict() + for z in self.rawData[i]['therm_schedules'][t]['zones']: + self.zones[nameHome][nameSchedule][z['id']] = z + + def homeById(self, hid): + return None if hid not in self.homes else self.homes[hid] + + def homeByName(self, home=None): + if not home: + home = self.default_home + for key, value in self.homes.items(): + if value['name'] == home: + return self.homes[key] + + def gethomeId(self, home=None): + if not home: + home = self.default_home + for key, value in self.homes.items(): + if value['name'] == home: + # print(self.homes[key]['id']) + # print(self.default_home) + return self.homes[key]['id'] + + def getSelectedschedule(self, home=None): + if not home: + home = self.default_home + self.schedule = self.schedules[home] + for key in self.schedule.keys(): + if 'selected' in self.schedule[key].keys(): + return self.schedule[key] + + +class HomeStatus(HomeData): + """ + """ + def __init__(self, authData, home_id=None, home=None): + self.getAuthToken = authData.accessToken + # print(self.modules()) + self.home_data = HomeData(authData) + # print(home_data.modules) + if home_id: + self.home_id = home_id + # print('home_id', self.home_id) + elif home: + self.home_id = self.home_data.gethomeId(home=home) + # print('self.home_id = self.home_data.gethomeId(home=home)', self.home_id) + else: + self.home_id = self.home_data.gethomeId(home=self.home_data.default_home) + # print('self.home_data.gethomeId(home=self.home_data.default_home)', self.home_id) + # self.default_home = self.home_data.default_home + postParams = { + "access_token": self.getAuthToken, + "home_id": self.home_id + } + + resp = postRequest(_GETHOMESTATUS_REQ, postParams) + # print(postParams) + # print(resp) + self.rawData = resp['body']['home'] + self.rooms = dict() + self.thermostats = dict() + self.valves = dict() + self.relays = dict() + for r in self.rawData['rooms']: + self.rooms[r['id']] = r + for t in range(len(self.rawData['modules'])): + if self.rawData['modules'][t]['type'] == 'NATherm1': + thermostatId = self.rawData['modules'][t]['id'] + if thermostatId not in self.thermostats: + self.thermostats[thermostatId] = dict() + self.thermostats[thermostatId] = self.rawData['modules'][t] + # self.thermostats[t['id']] = t + for v in range(len(self.rawData['modules'])): + if self.rawData['modules'][v]['type'] == 'NRV': + valveId = self.rawData['modules'][v]['id'] + if valveId not in self.valves: + self.valves[valveId] = dict() + self.valves[valveId] = self.rawData['modules'][v] + for r in range(len(self.rawData['modules'])): + if self.rawData['modules'][r]['type'] == 'NAPlug': + relayId = self.rawData['modules'][r]['id'] + if relayId not in self.relays: + self.relays[relayId] = dict() + self.relays[relayId] = self.rawData['modules'][r] + if self.rooms != {}: + self.default_room = list(self.rooms.values())[0] + if self.relays != {}: + self.default_relay = list(self.relays.values())[0] + if self.thermostats != {}: + self.default_thermostat = list(self.thermostats.values())[0] + print(self.thermostats) + if self.valves != {}: + self.default_valve = list(self.valves.values())[0] + # print(self.relays) + # print(self.default_room) + + def roomById(self, rid): + if not rid: + return self.default_room + for key, value in self.rooms.items(): + if value['id'] == rid: + return self.rooms[key] + + def thermostatById(self, rid): + if not rid: + return self.default_thermostat + for key, value in self.thermostats.items(): + if value['id'] == rid: + return self.thermostats[key] + + def relayById(self, rid): + if not rid: + return self.default_relay + for key, value in self.relays.items(): + if value['id'] == rid: + return self.relays[key] + + def valveById(self, rid): + if not rid: + return self.default_valve + for key, value in self.valves.items(): + if value['id'] == rid: + return self.valves[key] + + def setPoint(self, rid=None): + """ + Return the setpoint of a given room. + """ + setpoint = None + if rid: + room_data = self.roomById(rid=rid) + else: + room_data = self.roomById(rid=None) + if room_data: + setpoint = room_data['therm_setpoint_temperature'] + return setpoint + + def setPointmode(self, rid=None): + """ + Return the setpointmode of a given room. + """ + setpointmode = None + if rid: + room_data = self.roomById(rid=rid) + else: + room_data = self.roomById(rid=None) + if room_data: + setpointmode = room_data['therm_setpoint_mode'] + return setpointmode + + def getAwaytemp(self, home=None): + if not home: + home = self.home_data.default_home + # print(self.home_data.default_home) + data = self.home_data.getSelectedschedule(home=home) + return data['away_temp'] + + def getHgtemp(self, home=None): + if not home: + home = self.home_data.default_home + # print(self.home_data.default_home) + data = self.home_data.getSelectedschedule(home=home) + return data['hg_temp'] + + def measuredTemperature(self, rid=None): + """ + Return the measured temperature of a given room. + """ + temperature = None + print(rid) + if rid: + room_data = self.roomById(rid=rid) + else: + room_data = self.roomById(rid=None) + if room_data: + temperature = room_data['therm_measured_temperature'] + return temperature + + def boilerStatus(self, rid=None): + boiler_status = None + print(rid) + if rid: + relay_status = self.thermostatById(rid=rid) + else: + relay_status = self.thermostatById(rid=None) + if relay_status: + # print(self.thermostatById(rid=None)) + boiler_status = relay_status['boiler_status'] + return boiler_status + + def thermostatType(self, home, rid): + module_id = None + for key in self.home_data.rooms[home]: + if key == rid: + for module_id in self.home_data.rooms[home][rid]['module_ids']: + self.module_id = module_id + if module_id in self.thermostats: + return "NATherm1" + if module_id in self.valves: + return "NRV" + + def setThermmode(self, home_id, mode): + postParams = { + "access_token": self.getAuthToken, + "home_id": home_id, + "mode": mode + } + # print(postParams) + # return postRequest(_SETTHERMMODE_REQ, postParams) + resp = postRequest(_SETTHERMMODE_REQ, postParams) + print(resp) + + def setroomThermpoint(self, home_id, room_id, mode, temp=None): + postParams = { + "access_token": self.getAuthToken, + "home_id": home_id, + "room_id": room_id, + "mode": mode, + } + if temp is not None: + postParams['temp'] = temp + # print(postParams) + return postRequest(_SETROOMTHERMPOINT_REQ, postParams) + # print(resp) + class ThermostatData: """ From 25204cc31d312e0a30de38726d272dcd783fdd40 Mon Sep 17 00:00:00 2001 From: shanbs Date: Fri, 7 Dec 2018 14:55:25 +0100 Subject: [PATCH 030/333] Update Thermostat.py --- smart_home/Thermostat.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index cf8145c36..5f706757f 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -109,19 +109,14 @@ def __init__(self, authData, home_id=None, home=None): # print('home_id', self.home_id) elif home: self.home_id = self.home_data.gethomeId(home=home) - # print('self.home_id = self.home_data.gethomeId(home=home)', self.home_id) else: self.home_id = self.home_data.gethomeId(home=self.home_data.default_home) - # print('self.home_data.gethomeId(home=self.home_data.default_home)', self.home_id) - # self.default_home = self.home_data.default_home postParams = { "access_token": self.getAuthToken, "home_id": self.home_id } resp = postRequest(_GETHOMESTATUS_REQ, postParams) - # print(postParams) - # print(resp) self.rawData = resp['body']['home'] self.rooms = dict() self.thermostats = dict() @@ -157,8 +152,6 @@ def __init__(self, authData, home_id=None, home=None): print(self.thermostats) if self.valves != {}: self.default_valve = list(self.valves.values())[0] - # print(self.relays) - # print(self.default_room) def roomById(self, rid): if not rid: @@ -224,7 +217,6 @@ def getAwaytemp(self, home=None): def getHgtemp(self, home=None): if not home: home = self.home_data.default_home - # print(self.home_data.default_home) data = self.home_data.getSelectedschedule(home=home) return data['hg_temp'] @@ -250,7 +242,6 @@ def boilerStatus(self, rid=None): else: relay_status = self.thermostatById(rid=None) if relay_status: - # print(self.thermostatById(rid=None)) boiler_status = relay_status['boiler_status'] return boiler_status @@ -271,8 +262,6 @@ def setThermmode(self, home_id, mode): "home_id": home_id, "mode": mode } - # print(postParams) - # return postRequest(_SETTHERMMODE_REQ, postParams) resp = postRequest(_SETTHERMMODE_REQ, postParams) print(resp) @@ -285,9 +274,7 @@ def setroomThermpoint(self, home_id, room_id, mode, temp=None): } if temp is not None: postParams['temp'] = temp - # print(postParams) return postRequest(_SETROOMTHERMPOINT_REQ, postParams) - # print(resp) class ThermostatData: @@ -353,7 +340,7 @@ def lastData(self, device=None, exclude=0): ds = module['setpoint']['setpoint_mode'] dz = module['therm_program_list'][0]['zones'] if dm['time'] > limit : - lastD[module['module_name']] = dm.copy() # lastD['setpoint_mode'] = ds + lastD[module['module_name']] = dm.copy() lastD[module['module_name']]['setpoint_mode'] = ds # For potential use, add battery and radio coverage information to module data if present for i in ('battery_vp', 'rf_status', 'therm_relay_cmd', 'battery_percent') : From b1bb0193373948db99cebc0a0ef0639775fb590f Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 16 Dec 2018 21:35:57 +0100 Subject: [PATCH 031/333] v1.5 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 94116e9dd..1f3d23e39 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pyatmo', - version='1.4', # Should be updated with new versions + version='1.5', # Should be updated with new versions author='Hugo Dupras', author_email='jabesq@gmail.com', py_modules=['pyatmo'], From 1fe2e2914f2a7c8fffc67b46378fb0d3780e1126 Mon Sep 17 00:00:00 2001 From: Baosong Shan Date: Mon, 17 Dec 2018 22:31:15 +0100 Subject: [PATCH 032/333] Fix AttributeError if no camera installed (https://github.com/home-assistant/home-assistant/issues/16330). --- smart_home/Camera.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index 8363dc0e1..bfae2ac87 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -41,6 +41,8 @@ def __init__(self, authData, size=15): self.lastEvent = dict() self.outdoor_lastEvent = dict() self.types = dict() + self.default_home = None + self.default_camera = None for i in range(len(self.rawData['homes'])): nameHome = self.rawData['homes'][i]['name'] if nameHome not in self.cameras: @@ -78,7 +80,9 @@ def __init__(self, authData, size=15): self.default_module = list(self.modules.values())[0]['name'] else: self.default_module = None - self.default_camera = list(self.cameras[self.default_home].values())[0] + if self.default_home is not None and len(self.cameras) > 0: + self.default_camera = list(self.cameras[ + self.default_home].values())[0] def homeById(self, hid): return None if hid not in self.homes else self.homes[hid] From f13bfdba3389081d296bdcd356dcb2036b2b1839 Mon Sep 17 00:00:00 2001 From: Baosong Shan Date: Fri, 21 Dec 2018 15:35:36 +0100 Subject: [PATCH 033/333] Fix KeyError for a home without therm_schedules --- smart_home/Thermostat.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 5f706757f..58f858227 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -42,7 +42,6 @@ def __init__(self, authData): if 'modules' in self.rawData[i]: if nameHome not in self.modules: self.modules[nameHome] = dict() - self.default_home = self.rawData[i]['name'] for m in self.rawData[i]['modules']: self.modules[nameHome][m['id']] = m if nameHome not in self.rooms: @@ -54,19 +53,23 @@ def __init__(self, authData): if nameHome not in self.setpoint_duration: self.setpoint_duration[nameHome] = dict() if 'therm_setpoint_default_duration' in self.rawData[i]: - self.setpoint_duration[nameHome] = self.rawData[i]['therm_setpoint_default_duration'] + self.setpoint_duration[nameHome] = self.rawData[i][ + 'therm_setpoint_default_duration'] if 'rooms' in self.rawData[i]: for r in self.rawData[i]['rooms']: self.rooms[nameHome][r['id']] = r if 'therm_schedules' in self.rawData[i]: + self.default_home = self.rawData[i]['name'] for s in self.rawData[i]['therm_schedules']: self.schedules[nameHome][s['id']] = s - for t in range(len(self.rawData[i]['therm_schedules'])): - nameSchedule = self.rawData[i]['therm_schedules'][t]['name'] - if nameSchedule not in self.zones[nameHome]: - self.zones[nameHome][nameSchedule] = dict() - for z in self.rawData[i]['therm_schedules'][t]['zones']: - self.zones[nameHome][nameSchedule][z['id']] = z + for t in range(len(self.rawData[i]['therm_schedules'])): + nameSchedule = self.rawData[i]['therm_schedules'][ + t]['name'] + if nameSchedule not in self.zones[nameHome]: + self.zones[nameHome][nameSchedule] = dict() + for z in self.rawData[i]['therm_schedules'][t][ + 'zones']: + self.zones[nameHome][nameSchedule][z['id']] = z def homeById(self, hid): return None if hid not in self.homes else self.homes[hid] @@ -85,7 +88,8 @@ def gethomeId(self, home=None): if value['name'] == home: # print(self.homes[key]['id']) # print(self.default_home) - return self.homes[key]['id'] + if 'therm_schedules' in self.homes[key]: + return self.homes[key]['id'] def getSelectedschedule(self, home=None): if not home: From 1f05b63bb0c43452e367c0757716093dc440f13a Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Tue, 8 Jan 2019 23:28:26 +0100 Subject: [PATCH 034/333] pyatmo is now following black coding style Signed-off-by: Hugo D. (jabesq) --- pyatmo.py | 98 ++++++----- setup.py | 25 ++- smart_home/Camera.py | 312 ++++++++++++++++++----------------- smart_home/HomeCoach.py | 3 +- smart_home/PublicData.py | 56 ++++--- smart_home/Thermostat.py | 248 +++++++++++++++------------- smart_home/WeatherStation.py | 211 +++++++++++++---------- smart_home/__init__.py | 72 +++++--- 8 files changed, 563 insertions(+), 462 deletions(-) diff --git a/pyatmo.py b/pyatmo.py index f052beb04..ea3a4777f 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -25,16 +25,19 @@ # a Netatmo app in your Netatmo account. All you have to do is to give it a name (whatever) and you will be # returned a client_id and secret that your app has to supply to access netatmo servers. -_CLIENT_ID = "" # Your client ID from Netatmo app registration at http://dev.netatmo.com/dev/listapps -_CLIENT_SECRET = "" # Your client app secret ' ' -_USERNAME = "" # Your netatmo account username -_PASSWORD = "" # Your netatmo account password +_CLIENT_ID = ( + "" +) # Your client ID from Netatmo app registration at http://dev.netatmo.com/dev/listapps +_CLIENT_SECRET = "" # Your client app secret ' ' +_USERNAME = "" # Your netatmo account username +_PASSWORD = "" # Your netatmo account password ######################################################################### # Common definitions -_AUTH_REQ = _BASE_URL + "oauth2/token" +_AUTH_REQ = _BASE_URL + "oauth2/token" + class ClientAuth: """ @@ -55,41 +58,44 @@ class ClientAuth: Several value can be used at the same time, ie: 'read_station read_camera' """ - def __init__(self, clientId=_CLIENT_ID, - clientSecret=_CLIENT_SECRET, - username=_USERNAME, - password=_PASSWORD, - scope="read_station"): + def __init__( + self, + clientId=_CLIENT_ID, + clientSecret=_CLIENT_SECRET, + username=_USERNAME, + password=_PASSWORD, + scope="read_station", + ): postParams = { - "grant_type": "password", - "client_id": clientId, - "client_secret": clientSecret, - "username": username, - "password": password, - "scope": scope - } + "grant_type": "password", + "client_id": clientId, + "client_secret": clientSecret, + "username": username, + "password": password, + "scope": scope, + } resp = postRequest(_AUTH_REQ, postParams) self._clientId = clientId self._clientSecret = clientSecret - self._accessToken = resp['access_token'] - self.refreshToken = resp['refresh_token'] - self._scope = resp['scope'] - self.expiration = int(resp['expire_in'] + time.time() - 1800) + self._accessToken = resp["access_token"] + self.refreshToken = resp["refresh_token"] + self._scope = resp["scope"] + self.expiration = int(resp["expire_in"] + time.time() - 1800) @property def accessToken(self): if self.expiration < time.time(): # Token should be renewed postParams = { - "grant_type": "refresh_token", - "refresh_token": self.refreshToken, - "client_id": self._clientId, - "client_secret": self._clientSecret - } + "grant_type": "refresh_token", + "refresh_token": self.refreshToken, + "client_id": self._clientId, + "client_secret": self._clientSecret, + } resp = postRequest(_AUTH_REQ, postParams) - self._accessToken = resp['access_token'] - self.refreshToken = resp['refresh_token'] - self.expiration = int(resp['expire_in'] + time.time() - 1800) + self._accessToken = resp["access_token"] + self.refreshToken = resp["refresh_token"] + self.expiration = int(resp["expire_in"] + time.time() - 1800) return self._accessToken @@ -99,14 +105,14 @@ class User: Args: authData (ClientAuth): Authentication information with a working access Token """ + def __init__(self, authData): - postParams = { - "access_token" : authData.accessToken - } + postParams = {"access_token": authData.accessToken} resp = postRequest(_GETSTATIONDATA_REQ, postParams) - self.rawData = resp['body'] - self.devList = self.rawData['devices'] - self.ownerMail = self.rawData['user']['mail'] + self.rawData = resp["body"] + self.devList = self.rawData["devices"] + self.ownerMail = self.rawData["user"]["mail"] + # auto-test when executed directly @@ -114,34 +120,36 @@ def __init__(self, authData): from sys import exit, stdout, stderr - if not _CLIENT_ID or not _CLIENT_SECRET or not _USERNAME or not _PASSWORD : - stderr.write("Library source missing identification arguments to check lnetatmo.py (user/password/etc...)") - exit(1) + if not _CLIENT_ID or not _CLIENT_SECRET or not _USERNAME or not _PASSWORD: + stderr.write( + "Library source missing identification arguments to check lnetatmo.py (user/password/etc...)" + ) + exit(1) - authorization = ClientAuth(scope="read_station read_camera access_camera read_thermostat write_thermostat read_presence access_presence") # Test authentication method + authorization = ClientAuth( + scope="read_station read_camera access_camera read_thermostat write_thermostat read_presence access_presence" + ) # Test authentication method try: - devList = DeviceList(authorization) # Test DEVICELIST + devList = DeviceList(authorization) # Test DEVICELIST except NoDevice: if stdout.isatty(): print("lnetatmo.py : warning, no weather station available for testing") else: - devList.MinMaxTH() # Test GETMEASUR - + devList.MinMaxTH() # Test GETMEASUR try: Camera = CameraData(authorization) - except NoDevice : + except NoDevice: if stdout.isatty(): print("lnetatmo.py : warning, no camera available for testing") try: Thermostat = ThermostatData(authorization) - except NoDevice : + except NoDevice: if stdout.isatty(): print("lnetatmo.py : warning, no thermostat available for testing") - PublicData(authorization) # If we reach this line, all is OK diff --git a/setup.py b/setup.py index 1f3d23e39..11823b71f 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ - # python setup.py --dry-run --verbose install from distutils.core import setup @@ -6,18 +5,18 @@ setup( - name='pyatmo', - version='1.5', # Should be updated with new versions - author='Hugo Dupras', - author_email='jabesq@gmail.com', - py_modules=['pyatmo'], - packages=['smart_home'], - package_dir={'smart_home': 'smart_home'}, + name="pyatmo", + version="1.5", # Should be updated with new versions + author="Hugo Dupras", + author_email="jabesq@gmail.com", + py_modules=["pyatmo"], + packages=["smart_home"], + package_dir={"smart_home": "smart_home"}, scripts=[], data_files=[], - url='https://github.com/jabesq/netatmo-api-python', - license='MIT', - description='Simple API to access Netatmo weather station data from any python script. ' - 'Design for Home-Assitant (but not only)', - long_description=open('README.md').read() + url="https://github.com/jabesq/netatmo-api-python", + license="MIT", + description="Simple API to access Netatmo weather station data from any python script. " + "Design for Home-Assitant (but not only)", + long_description=open("README.md").read(), ) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index bfae2ac87..b0bef04c6 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -20,17 +20,15 @@ class CameraData: authData (ClientAuth): Authentication information with a working access Token """ + def __init__(self, authData, size=15): self.getAuthToken = authData.accessToken - postParams = { - "access_token": self.getAuthToken, - "size": size - } + postParams = {"access_token": self.getAuthToken, "size": size} resp = postRequest(_GETHOMEDATA_REQ, postParams) - if 'body' not in resp: - raise URLError('No data returned by Netatmo server') - self.rawData = resp['body'] - self.homes = {d['id']: d for d in self.rawData['homes']} + if "body" not in resp: + raise URLError("No data returned by Netatmo server") + self.rawData = resp["body"] + self.homes = {d["id"]: d for d in self.rawData["homes"]} if not self.homes: raise NoDevice("No camera available") self.persons = dict() @@ -43,46 +41,47 @@ def __init__(self, authData, size=15): self.types = dict() self.default_home = None self.default_camera = None - for i in range(len(self.rawData['homes'])): - nameHome = self.rawData['homes'][i]['name'] + for i in range(len(self.rawData["homes"])): + nameHome = self.rawData["homes"][i]["name"] if nameHome not in self.cameras: self.cameras[nameHome] = dict() if nameHome not in self.types: self.types[nameHome] = dict() - for p in self.rawData['homes'][i]['persons']: - self.persons[p['id']] = p - if 'events' in self.rawData['homes'][i]: - self.default_home = self.rawData['homes'][i]['name'] - for e in self.rawData['homes'][i]['events']: - if e['type'] == 'outdoor': - if e['camera_id'] not in self.outdoor_events: - self.outdoor_events[e['camera_id']] = dict() - self.outdoor_events[e['camera_id']][e['time']] = e - elif e['type'] != 'outdoor': - if e['camera_id'] not in self.events: - self.events[e['camera_id']] = dict() - self.events[e['camera_id']][e['time']] = e - for c in self.rawData['homes'][i]['cameras']: - self.cameras[nameHome][c['id']] = c - if c['type'] == 'NACamera' and 'modules' in c: - for m in c['modules']: - self.modules[m['id']] = m - self.modules[m['id']]['cam_id'] = c['id'] - for t in self.rawData['homes'][i]['cameras']: - self.types[nameHome][t['type']] = t + for p in self.rawData["homes"][i]["persons"]: + self.persons[p["id"]] = p + if "events" in self.rawData["homes"][i]: + self.default_home = self.rawData["homes"][i]["name"] + for e in self.rawData["homes"][i]["events"]: + if e["type"] == "outdoor": + if e["camera_id"] not in self.outdoor_events: + self.outdoor_events[e["camera_id"]] = dict() + self.outdoor_events[e["camera_id"]][e["time"]] = e + elif e["type"] != "outdoor": + if e["camera_id"] not in self.events: + self.events[e["camera_id"]] = dict() + self.events[e["camera_id"]][e["time"]] = e + for c in self.rawData["homes"][i]["cameras"]: + self.cameras[nameHome][c["id"]] = c + if c["type"] == "NACamera" and "modules" in c: + for m in c["modules"]: + self.modules[m["id"]] = m + self.modules[m["id"]]["cam_id"] = c["id"] + for t in self.rawData["homes"][i]["cameras"]: + self.types[nameHome][t["type"]] = t for camera in self.events: self.lastEvent[camera] = self.events[camera][ - sorted(self.events[camera])[-1]] + sorted(self.events[camera])[-1] + ] for camera in self.outdoor_events: self.outdoor_lastEvent[camera] = self.outdoor_events[camera][ - sorted(self.outdoor_events[camera])[-1]] + sorted(self.outdoor_events[camera])[-1] + ] if self.modules != {}: - self.default_module = list(self.modules.values())[0]['name'] + self.default_module = list(self.modules.values())[0]["name"] else: self.default_module = None if self.default_home is not None and len(self.cameras) > 0: - self.default_camera = list(self.cameras[ - self.default_home].values())[0] + self.default_camera = list(self.cameras[self.default_home].values())[0] def homeById(self, hid): return None if hid not in self.homes else self.homes[hid] @@ -91,7 +90,7 @@ def homeByName(self, home=None): if not home: return self.homeByName(self.default_home) for key, value in self.homes.items(): - if value['name'] == home: + if value["name"] == home: return self.homes[key] def cameraById(self, cid): @@ -107,12 +106,12 @@ def cameraByName(self, camera=None, home=None): if home not in self.cameras: return None for cam_id in self.cameras[home]: - if self.cameras[home][cam_id]['name'] == camera: + if self.cameras[home][cam_id]["name"] == camera: return self.cameras[home][cam_id] elif not home and camera: for home, cam_ids in self.cameras.items(): for cam_id in cam_ids: - if self.cameras[home][cam_id]['name'] == camera: + if self.cameras[home][cam_id]["name"] == camera: return self.cameras[home][cam_id] else: return list(self.cameras[home].values())[0] @@ -133,8 +132,8 @@ def moduleByName(self, module=None, camera=None, home=None): if not cam: return None for key, value in self.modules.items(): - if value['name'] == module: - if cam and value['cam_id'] != cam['id']: + if value["name"] == module: + if cam and value["cam_id"] != cam["id"]: return None return self.modules[key] return None @@ -149,7 +148,7 @@ def cameraType(self, camera=None, home=None, cid=None): else: camera_data = self.cameraByName(camera=camera, home=home) if camera_data: - cameratype = camera_data['type'] + cameratype = camera_data["type"] return cameratype def cameraUrls(self, camera=None, home=None, cid=None): @@ -164,19 +163,21 @@ def cameraUrls(self, camera=None, home=None, cid=None): else: camera_data = self.cameraByName(camera=camera, home=home) if camera_data: - vpn_url = camera_data['vpn_url'] - if camera_data['is_local']: + vpn_url = camera_data["vpn_url"] + if camera_data["is_local"]: try: - resp = postRequest('{0}/command/ping'.format( - camera_data['vpn_url']), dict()) - temp_local_url = resp['local_url'] + resp = postRequest( + "{0}/command/ping".format(camera_data["vpn_url"]), dict() + ) + temp_local_url = resp["local_url"] except URLError: return None, None try: - resp = postRequest('{0}/command/ping'.format( - temp_local_url), dict()) - if temp_local_url == resp['local_url']: + resp = postRequest( + "{0}/command/ping".format(temp_local_url), dict() + ) + if temp_local_url == resp["local_url"]: local_url = temp_local_url except URLError: pass @@ -190,11 +191,11 @@ def personsAtHome(self, home=None): home = self.default_home home_data = self.homeByName(home) atHome = [] - for p in home_data['persons']: + for p in home_data["persons"]: # Only check known persons - if 'pseudo' in p: + if "pseudo" in p: if not p["out_of_sight"]: - atHome.append(p['pseudo']) + atHome.append(p["pseudo"]) return atHome def getCameraPicture(self, image_id, key): @@ -204,10 +205,10 @@ def getCameraPicture(self, image_id, key): postParams = { "access_token": self.getAuthToken, "image_id": image_id, - "key": key - } + "key": key, + } resp = postRequest(_GETCAMERAPICTURE_REQ, postParams) - image_type = imghdr.what('NONE.FILE', resp) + image_type = imghdr.what("NONE.FILE", resp) return resp, image_type def getProfileImage(self, name): @@ -215,10 +216,10 @@ def getProfileImage(self, name): Retrieve the face of a given person """ for p in self.persons: - if 'pseudo' in self.persons[p]: - if name == self.persons[p]['pseudo']: - image_id = self.persons[p]['face']['id'] - key = self.persons[p]['face']['key'] + if "pseudo" in self.persons[p]: + if name == self.persons[p]["pseudo"]: + image_id = self.persons[p]["face"]["id"] + key = self.persons[p]["face"]["key"] return self.getCameraPicture(image_id, key) return None, None @@ -228,87 +229,89 @@ def updateEvent(self, event=None, home=None, cameratype=None): """ if not home: home = self.default_home - if cameratype == 'NACamera': + if cameratype == "NACamera": # for the Welcome camera if not event: # If not event is provided we need to retrieve the oldest of # the last event seen by each camera listEvent = dict() for cam_id in self.lastEvent: - listEvent[self.lastEvent[cam_id]['time']] =\ - self.lastEvent[cam_id] + listEvent[self.lastEvent[cam_id]["time"]] = self.lastEvent[cam_id] event = listEvent[sorted(listEvent)[0]] - if cameratype == 'NOC': + if cameratype == "NOC": # for the Presence camera if not event: # If not event is provided we need to retrieve the oldest of # the last event seen by each camera listEvent = dict() for cam_id in self.outdoor_lastEvent: - listEvent[self.outdoor_lastEvent[cam_id]['time']] =\ - self.outdoor_lastEvent[cam_id] + listEvent[ + self.outdoor_lastEvent[cam_id]["time"] + ] = self.outdoor_lastEvent[cam_id] event = listEvent[sorted(listEvent)[0]] home_data = self.homeByName(home) postParams = { - "access_token": self.getAuthToken, - "home_id": home_data['id'], - "event_id": event['id'] - } + "access_token": self.getAuthToken, + "home_id": home_data["id"], + "event_id": event["id"], + } resp = postRequest(_GETEVENTSUNTIL_REQ, postParams) - eventList = resp['body']['events_list'] + eventList = resp["body"]["events_list"] for e in eventList: - if e['type'] == 'outdoor': - self.outdoor_events[e['camera_id']][e['time']] = e - elif e['type'] != 'outdoor': - self.events[e['camera_id']][e['time']] = e + if e["type"] == "outdoor": + self.outdoor_events[e["camera_id"]][e["time"]] = e + elif e["type"] != "outdoor": + self.events[e["camera_id"]][e["time"]] = e for camera in self.events: - self.lastEvent[camera] = self.events[camera][ - sorted(self.events[camera])[-1]] + self.lastEvent[camera] = self.events[camera][ + sorted(self.events[camera])[-1] + ] for camera in self.outdoor_events: - self.outdoor_lastEvent[camera] = self.outdoor_events[camera][ - sorted(self.outdoor_events[camera])[-1]] + self.outdoor_lastEvent[camera] = self.outdoor_events[camera][ + sorted(self.outdoor_events[camera])[-1] + ] def personSeenByCamera(self, name, home=None, camera=None, exclude=0): """ Return True if a specific person has been seen by a camera """ try: - cam_id = self.cameraByName(camera=camera, home=home)['id'] + cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: print("personSeenByCamera: Camera name or home is unknown") return False # Check in the last event is someone known has been seen if exclude: - limit = (time.time() - exclude) + limit = time.time() - exclude array_time_event = sorted(self.events[cam_id]) array_time_event.reverse() for time_ev in array_time_event: if time_ev < limit: return False - elif self.events[cam_id][time_ev]['type'] == 'person': - person_id = self.events[cam_id][time_ev]['person_id'] - if 'pseudo' in self.persons[person_id]: - if self.persons[person_id]['pseudo'] == name: + elif self.events[cam_id][time_ev]["type"] == "person": + person_id = self.events[cam_id][time_ev]["person_id"] + if "pseudo" in self.persons[person_id]: + if self.persons[person_id]["pseudo"] == name: return True - elif self.lastEvent[cam_id]['type'] == 'person': - person_id = self.lastEvent[cam_id]['person_id'] - if 'pseudo' in self.persons[person_id]: - if self.persons[person_id]['pseudo'] == name: + elif self.lastEvent[cam_id]["type"] == "person": + person_id = self.lastEvent[cam_id]["person_id"] + if "pseudo" in self.persons[person_id]: + if self.persons[person_id]["pseudo"] == name: return True return False def _knownPersons(self): known_persons = dict() for p_id, p in self.persons.items(): - if 'pseudo' in p: + if "pseudo" in p: known_persons[p_id] = p return known_persons def knownPersonsNames(self): names = [] for p_id, p in self._knownPersons().items(): - names.append(p['pseudo']) + names.append(p["pseudo"]) return names def someoneKnownSeen(self, home=None, camera=None, exclude=0): @@ -316,25 +319,27 @@ def someoneKnownSeen(self, home=None, camera=None, exclude=0): Return True if someone known has been seen """ try: - cam_id = self.cameraByName(camera=camera, home=home)['id'] + cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: print("someoneKnownSeen: Camera name or home is unknown") return False if exclude: - limit = (time.time() - exclude) + limit = time.time() - exclude array_time_event = sorted(self.events[cam_id]) array_time_event.reverse() for time_ev in array_time_event: if time_ev < limit: return False - elif self.events[cam_id][time_ev]['type'] == 'person': - if self.events[cam_id][time_ev][ - 'person_id'] in self._knownPersons(): + elif self.events[cam_id][time_ev]["type"] == "person": + if ( + self.events[cam_id][time_ev]["person_id"] + in self._knownPersons() + ): return True # Check in the last event is someone known has been seen - elif self.lastEvent[cam_id]['type'] == 'person': - if self.lastEvent[cam_id]['person_id'] in self._knownPersons(): + elif self.lastEvent[cam_id]["type"] == "person": + if self.lastEvent[cam_id]["person_id"] in self._knownPersons(): return True return False @@ -343,25 +348,27 @@ def someoneUnknownSeen(self, home=None, camera=None, exclude=0): Return True if someone unknown has been seen """ try: - cam_id = self.cameraByName(camera=camera, home=home)['id'] + cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: print("someoneUnknownSeen: Camera name or home is unknown") return False if exclude: - limit = (time.time() - exclude) + limit = time.time() - exclude array_time_event = sorted(self.events[cam_id]) array_time_event.reverse() for time_ev in array_time_event: if time_ev < limit: return False - elif self.events[cam_id][time_ev]['type'] == 'person': - if self.events[cam_id][time_ev][ - 'person_id'] not in self._knownPersons(): + elif self.events[cam_id][time_ev]["type"] == "person": + if ( + self.events[cam_id][time_ev]["person_id"] + not in self._knownPersons() + ): return True # Check in the last event is someone known has been seen - elif self.lastEvent[cam_id]['type'] == 'person': - if self.lastEvent[cam_id]['person_id'] not in self._knownPersons(): + elif self.lastEvent[cam_id]["type"] == "person": + if self.lastEvent[cam_id]["person_id"] not in self._knownPersons(): return True return False @@ -370,21 +377,21 @@ def motionDetected(self, home=None, camera=None, exclude=0): Return True if movement has been detected """ try: - cam_id = self.cameraByName(camera=camera, home=home)['id'] + cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: print("motionDetected: Camera name or home is unknown") return False if exclude: - limit = (time.time() - exclude) + limit = time.time() - exclude array_time_event = sorted(self.events[cam_id]) array_time_event.reverse() for time_ev in array_time_event: if time_ev < limit: return False - elif self.events[cam_id][time_ev]['type'] == 'movement': + elif self.events[cam_id][time_ev]["type"] == "movement": return True - elif self.lastEvent[cam_id]['type'] == 'movement': + elif self.lastEvent[cam_id]["type"] == "movement": return True return False @@ -393,14 +400,17 @@ def outdoormotionDetected(self, home=None, camera=None, offset=0): Return True if outdoor movement has been detected """ try: - cam_id = self.cameraByName(camera=camera, home=home)['id'] + cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: print("outdoormotionDetected: Camera name or home is unknown") return False if cam_id in self.lastEvent: - if self.lastEvent[cam_id]['type'] == 'movement': - if self.lastEvent[cam_id]['video_status'] == 'recording' and\ - self.lastEvent[cam_id]['time'] + offset > int(time.time()): + if self.lastEvent[cam_id]["type"] == "movement": + if self.lastEvent[cam_id][ + "video_status" + ] == "recording" and self.lastEvent[cam_id]["time"] + offset > int( + time.time() + ): return True return False @@ -409,14 +419,13 @@ def humanDetected(self, home=None, camera=None, offset=0): Return True if a human has been detected """ try: - cam_id = self.cameraByName(camera=camera, home=home)['id'] + cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: print("personSeenByCamera: Camera name or home is unknown") return False - if self.outdoor_lastEvent[cam_id]['video_status'] == 'recording': - for e in self.outdoor_lastEvent[cam_id]['event_list']: - if e['type'] ==\ - 'human' and e['time'] + offset > int(time.time()): + if self.outdoor_lastEvent[cam_id]["video_status"] == "recording": + for e in self.outdoor_lastEvent[cam_id]["event_list"]: + if e["type"] == "human" and e["time"] + offset > int(time.time()): return True return False @@ -425,15 +434,14 @@ def animalDetected(self, home=None, camera=None, offset=0): Return True if an animal has been detected """ try: - cam_id = self.cameraByName(camera=camera, home=home)['id'] + cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: print("animalDetected: Camera name or home is unknown") return False - if self.outdoor_lastEvent[cam_id]['video_status'] == 'recording': - for e in self.outdoor_lastEvent[cam_id]['event_list']: - if e['type'] ==\ - 'animal' and e['time'] + offset > int(time.time()): + if self.outdoor_lastEvent[cam_id]["video_status"] == "recording": + for e in self.outdoor_lastEvent[cam_id]["event_list"]: + if e["type"] == "animal" and e["time"] + offset > int(time.time()): return True return False @@ -442,48 +450,48 @@ def carDetected(self, home=None, camera=None, offset=0): Return True if a car has been detected """ try: - cam_id = self.cameraByName(camera=camera, home=home)['id'] + cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: print("carDetected: Camera name or home is unknown") return False - if self.outdoor_lastEvent[cam_id]['video_status'] == 'recording': - for e in self.outdoor_lastEvent[cam_id]['event_list']: - if e['type'] ==\ - 'vehicle' and e['time'] + offset > int(time.time()): + if self.outdoor_lastEvent[cam_id]["video_status"] == "recording": + for e in self.outdoor_lastEvent[cam_id]["event_list"]: + if e["type"] == "vehicle" and e["time"] + offset > int(time.time()): return True return False - def moduleMotionDetected(self, module=None, home=None, - camera=None, exclude=0): + def moduleMotionDetected(self, module=None, home=None, camera=None, exclude=0): """ Return True if movement has been detected """ try: mod = self.moduleByName(module, camera=camera, home=home) - mod_id = mod['id'] - cam_id = mod['cam_id'] + mod_id = mod["id"] + cam_id = mod["cam_id"] except TypeError: - print("moduleMotionDetected: Module name or" - "Camera name or home is unknown") + print( + "moduleMotionDetected: Module name or" "Camera name or home is unknown" + ) return False if exclude: - limit = (time.time() - exclude) + limit = time.time() - exclude array_time_event = sorted(self.events[cam_id]) array_time_event.reverse() for time_ev in array_time_event: if time_ev < limit: return False - elif (self.events[cam_id][time_ev]['type'] == 'tag_big_move' - or self.events[cam_id][time_ev]['type'] == - 'tag_small_move') and\ - self.events[cam_id][time_ev]['module_id'] == mod_id: - return True - elif (self.lastEvent[cam_id]['type'] == 'tag_big_move' or - self.lastEvent[cam_id]['type'] == 'tag_small_move') and\ - self.lastEvent[cam_id]['module_id'] == mod_id: + elif ( + self.events[cam_id][time_ev]["type"] == "tag_big_move" + or self.events[cam_id][time_ev]["type"] == "tag_small_move" + ) and self.events[cam_id][time_ev]["module_id"] == mod_id: return True + elif ( + self.lastEvent[cam_id]["type"] == "tag_big_move" + or self.lastEvent[cam_id]["type"] == "tag_small_move" + ) and self.lastEvent[cam_id]["module_id"] == mod_id: + return True return False def moduleOpened(self, module=None, home=None, camera=None, exclude=0): @@ -492,23 +500,27 @@ def moduleOpened(self, module=None, home=None, camera=None, exclude=0): """ try: mod = self.moduleByName(module, camera=camera, home=home) - mod_id = mod['id'] - cam_id = mod['cam_id'] + mod_id = mod["id"] + cam_id = mod["cam_id"] except TypeError: print("moduleOpened: Camera name, or home, or module is unknown") return False if exclude: - limit = (time.time() - exclude) + limit = time.time() - exclude array_time_event = sorted(self.events[cam_id]) array_time_event.reverse() for time_ev in array_time_event: if time_ev < limit: return False - elif self.events[cam_id][time_ev]['type'] == 'tag_open' and\ - self.events[cam_id][time_ev]['module_id'] == mod_id: + elif ( + self.events[cam_id][time_ev]["type"] == "tag_open" + and self.events[cam_id][time_ev]["module_id"] == mod_id + ): return True - elif self.lastEvent[cam_id]['type'] == 'tag_open' and\ - self.lastEvent[cam_id]['module_id'] == mod_id: + elif ( + self.lastEvent[cam_id]["type"] == "tag_open" + and self.lastEvent[cam_id]["module_id"] == mod_id + ): return True return False diff --git a/smart_home/HomeCoach.py b/smart_home/HomeCoach.py index 07d345ef9..f0d290cd8 100644 --- a/smart_home/HomeCoach.py +++ b/smart_home/HomeCoach.py @@ -14,5 +14,6 @@ class HomeCoachData(WeatherStationData): Args: authData (ClientAuth): Authentication information with a working access Token """ + def __init__(self, authData): - super(HomeCoachData, self).__init__(authData, urlReq=_GETHOMECOACHDATA_REQ) + super(HomeCoachData, self).__init__(authData, urlReq=_GETHOMECOACHDATA_REQ) diff --git a/smart_home/PublicData.py b/smart_home/PublicData.py index f36c9357d..9c4bd6936 100644 --- a/smart_home/PublicData.py +++ b/smart_home/PublicData.py @@ -27,15 +27,16 @@ class PublicData: - - def __init__(self, - auth_data, - LAT_NE=_LAT_NE, - LON_NE=_LON_NE, - LAT_SW=_LAT_SW, - LON_SW=_LON_SW, - required_data_type=None, # comma-separated list from above _STATION or _ACCESSORY values - filtering=False): + def __init__( + self, + auth_data, + LAT_NE=_LAT_NE, + LON_NE=_LON_NE, + LAT_SW=_LAT_SW, + LON_SW=_LON_SW, + required_data_type=None, # comma-separated list from above _STATION or _ACCESSORY values + filtering=False, + ): self.getAuthToken = auth_data.accessToken post_params = { "access_token": self.getAuthToken, @@ -43,17 +44,17 @@ def __init__(self, "lon_ne": LON_NE, "lat_sw": LAT_SW, "lon_sw": LON_SW, - "filter": filtering - } + "filter": filtering, + } if required_data_type: - post_params['required_data'] = required_data_type + post_params["required_data"] = required_data_type resp = postRequest(_GETPUBLIC_DATA, post_params) - self.raw_data = resp['body'] - self.status = resp['status'] - self.time_exec = toTimeString(resp['time_exec']) - self.time_server = toTimeString(resp['time_server']) + self.raw_data = resp["body"] + self.status = resp["status"] + self.time_exec = toTimeString(resp["time_exec"]) + self.time_server = toTimeString(resp["time_server"]) def CountStationInArea(self): return len(self.raw_data) @@ -127,7 +128,7 @@ def getLatestGustAngles(self): def getLocations(self): locations = {} for station in self.raw_data: - locations[station['_id']] = station['place']['location'] + locations[station["_id"]] = station["place"]["location"] return locations # Backwards compatibility for < 1.2 @@ -143,19 +144,26 @@ def getTimeForWindMeasures(self): def getLatestStationMeasures(self, type): measures = {} for station in self.raw_data: - for _, module in station['measures'].items(): - if 'type' in module and type in module['type'] and 'res' in module and module['res']: - measure_index = module['type'].index(type) - latest_timestamp = sorted(module['res'], reverse=True)[0] - measures[station['_id']] = module['res'][latest_timestamp][measure_index] + for _, module in station["measures"].items(): + if ( + "type" in module + and type in module["type"] + and "res" in module + and module["res"] + ): + measure_index = module["type"].index(type) + latest_timestamp = sorted(module["res"], reverse=True)[0] + measures[station["_id"]] = module["res"][latest_timestamp][ + measure_index + ] return measures def getAccessoryMeasures(self, type): measures = {} for station in self.raw_data: - for _, module in station['measures'].items(): + for _, module in station["measures"].items(): if type in module: - measures[station['_id']] = module[type] + measures[station["_id"]] = module[type] return measures diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 58f858227..15161d964 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -6,7 +6,7 @@ from . import NoDevice, postRequest, _BASE_URL _SETTEMP_REQ = _BASE_URL + "api/setthermpoint" -_GETTHERMOSTATDATA_REQ = _BASE_URL + "api/getthermostatsdata" +_GETTHERMOSTATDATA_REQ = _BASE_URL + "api/getthermostatsdata" _GETHOMESDATA_REQ = _BASE_URL + "api/homesdata" _GETHOMESTATUS_REQ = _BASE_URL + "api/homestatus" _SETTHERMMODE_REQ = _BASE_URL + "api/setthermmode" @@ -21,15 +21,14 @@ class HomeData: Args: authData (ClientAuth): Authentication information with a working access Token """ + def __init__(self, authData): self.getAuthToken = authData.accessToken - postParams = { - "access_token": self.getAuthToken - } + postParams = {"access_token": self.getAuthToken} resp = postRequest(_GETHOMESDATA_REQ, postParams) - self.rawData = resp['body']['homes'] - self.homes = {d['id']: d for d in self.rawData} + self.rawData = resp["body"]["homes"] + self.homes = {d["id"]: d for d in self.rawData} if not self.homes: raise NoDevice("No thermostat available") self.modules = dict() @@ -38,12 +37,12 @@ def __init__(self, authData): self.zones = dict() self.setpoint_duration = dict() for i in range(len(self.rawData)): - nameHome = self.rawData[i]['name'] - if 'modules' in self.rawData[i]: + nameHome = self.rawData[i]["name"] + if "modules" in self.rawData[i]: if nameHome not in self.modules: self.modules[nameHome] = dict() - for m in self.rawData[i]['modules']: - self.modules[nameHome][m['id']] = m + for m in self.rawData[i]["modules"]: + self.modules[nameHome][m["id"]] = m if nameHome not in self.rooms: self.rooms[nameHome] = dict() if nameHome not in self.schedules: @@ -52,24 +51,23 @@ def __init__(self, authData): self.zones[nameHome] = dict() if nameHome not in self.setpoint_duration: self.setpoint_duration[nameHome] = dict() - if 'therm_setpoint_default_duration' in self.rawData[i]: + if "therm_setpoint_default_duration" in self.rawData[i]: self.setpoint_duration[nameHome] = self.rawData[i][ - 'therm_setpoint_default_duration'] - if 'rooms' in self.rawData[i]: - for r in self.rawData[i]['rooms']: - self.rooms[nameHome][r['id']] = r - if 'therm_schedules' in self.rawData[i]: - self.default_home = self.rawData[i]['name'] - for s in self.rawData[i]['therm_schedules']: - self.schedules[nameHome][s['id']] = s - for t in range(len(self.rawData[i]['therm_schedules'])): - nameSchedule = self.rawData[i]['therm_schedules'][ - t]['name'] + "therm_setpoint_default_duration" + ] + if "rooms" in self.rawData[i]: + for r in self.rawData[i]["rooms"]: + self.rooms[nameHome][r["id"]] = r + if "therm_schedules" in self.rawData[i]: + self.default_home = self.rawData[i]["name"] + for s in self.rawData[i]["therm_schedules"]: + self.schedules[nameHome][s["id"]] = s + for t in range(len(self.rawData[i]["therm_schedules"])): + nameSchedule = self.rawData[i]["therm_schedules"][t]["name"] if nameSchedule not in self.zones[nameHome]: self.zones[nameHome][nameSchedule] = dict() - for z in self.rawData[i]['therm_schedules'][t][ - 'zones']: - self.zones[nameHome][nameSchedule][z['id']] = z + for z in self.rawData[i]["therm_schedules"][t]["zones"]: + self.zones[nameHome][nameSchedule][z["id"]] = z def homeById(self, hid): return None if hid not in self.homes else self.homes[hid] @@ -78,31 +76,32 @@ def homeByName(self, home=None): if not home: home = self.default_home for key, value in self.homes.items(): - if value['name'] == home: + if value["name"] == home: return self.homes[key] def gethomeId(self, home=None): if not home: home = self.default_home for key, value in self.homes.items(): - if value['name'] == home: + if value["name"] == home: # print(self.homes[key]['id']) # print(self.default_home) - if 'therm_schedules' in self.homes[key]: - return self.homes[key]['id'] + if "therm_schedules" in self.homes[key]: + return self.homes[key]["id"] def getSelectedschedule(self, home=None): if not home: home = self.default_home self.schedule = self.schedules[home] for key in self.schedule.keys(): - if 'selected' in self.schedule[key].keys(): + if "selected" in self.schedule[key].keys(): return self.schedule[key] class HomeStatus(HomeData): """ """ + def __init__(self, authData, home_id=None, home=None): self.getAuthToken = authData.accessToken # print(self.modules()) @@ -115,38 +114,35 @@ def __init__(self, authData, home_id=None, home=None): self.home_id = self.home_data.gethomeId(home=home) else: self.home_id = self.home_data.gethomeId(home=self.home_data.default_home) - postParams = { - "access_token": self.getAuthToken, - "home_id": self.home_id - } + postParams = {"access_token": self.getAuthToken, "home_id": self.home_id} resp = postRequest(_GETHOMESTATUS_REQ, postParams) - self.rawData = resp['body']['home'] + self.rawData = resp["body"]["home"] self.rooms = dict() self.thermostats = dict() self.valves = dict() self.relays = dict() - for r in self.rawData['rooms']: - self.rooms[r['id']] = r - for t in range(len(self.rawData['modules'])): - if self.rawData['modules'][t]['type'] == 'NATherm1': - thermostatId = self.rawData['modules'][t]['id'] + for r in self.rawData["rooms"]: + self.rooms[r["id"]] = r + for t in range(len(self.rawData["modules"])): + if self.rawData["modules"][t]["type"] == "NATherm1": + thermostatId = self.rawData["modules"][t]["id"] if thermostatId not in self.thermostats: self.thermostats[thermostatId] = dict() - self.thermostats[thermostatId] = self.rawData['modules'][t] + self.thermostats[thermostatId] = self.rawData["modules"][t] # self.thermostats[t['id']] = t - for v in range(len(self.rawData['modules'])): - if self.rawData['modules'][v]['type'] == 'NRV': - valveId = self.rawData['modules'][v]['id'] + for v in range(len(self.rawData["modules"])): + if self.rawData["modules"][v]["type"] == "NRV": + valveId = self.rawData["modules"][v]["id"] if valveId not in self.valves: self.valves[valveId] = dict() - self.valves[valveId] = self.rawData['modules'][v] - for r in range(len(self.rawData['modules'])): - if self.rawData['modules'][r]['type'] == 'NAPlug': - relayId = self.rawData['modules'][r]['id'] + self.valves[valveId] = self.rawData["modules"][v] + for r in range(len(self.rawData["modules"])): + if self.rawData["modules"][r]["type"] == "NAPlug": + relayId = self.rawData["modules"][r]["id"] if relayId not in self.relays: self.relays[relayId] = dict() - self.relays[relayId] = self.rawData['modules'][r] + self.relays[relayId] = self.rawData["modules"][r] if self.rooms != {}: self.default_room = list(self.rooms.values())[0] if self.relays != {}: @@ -161,28 +157,28 @@ def roomById(self, rid): if not rid: return self.default_room for key, value in self.rooms.items(): - if value['id'] == rid: + if value["id"] == rid: return self.rooms[key] def thermostatById(self, rid): if not rid: return self.default_thermostat for key, value in self.thermostats.items(): - if value['id'] == rid: + if value["id"] == rid: return self.thermostats[key] def relayById(self, rid): if not rid: return self.default_relay for key, value in self.relays.items(): - if value['id'] == rid: + if value["id"] == rid: return self.relays[key] def valveById(self, rid): if not rid: return self.default_valve for key, value in self.valves.items(): - if value['id'] == rid: + if value["id"] == rid: return self.valves[key] def setPoint(self, rid=None): @@ -195,7 +191,7 @@ def setPoint(self, rid=None): else: room_data = self.roomById(rid=None) if room_data: - setpoint = room_data['therm_setpoint_temperature'] + setpoint = room_data["therm_setpoint_temperature"] return setpoint def setPointmode(self, rid=None): @@ -208,7 +204,7 @@ def setPointmode(self, rid=None): else: room_data = self.roomById(rid=None) if room_data: - setpointmode = room_data['therm_setpoint_mode'] + setpointmode = room_data["therm_setpoint_mode"] return setpointmode def getAwaytemp(self, home=None): @@ -216,13 +212,13 @@ def getAwaytemp(self, home=None): home = self.home_data.default_home # print(self.home_data.default_home) data = self.home_data.getSelectedschedule(home=home) - return data['away_temp'] + return data["away_temp"] def getHgtemp(self, home=None): if not home: home = self.home_data.default_home data = self.home_data.getSelectedschedule(home=home) - return data['hg_temp'] + return data["hg_temp"] def measuredTemperature(self, rid=None): """ @@ -235,7 +231,7 @@ def measuredTemperature(self, rid=None): else: room_data = self.roomById(rid=None) if room_data: - temperature = room_data['therm_measured_temperature'] + temperature = room_data["therm_measured_temperature"] return temperature def boilerStatus(self, rid=None): @@ -246,14 +242,14 @@ def boilerStatus(self, rid=None): else: relay_status = self.thermostatById(rid=None) if relay_status: - boiler_status = relay_status['boiler_status'] + boiler_status = relay_status["boiler_status"] return boiler_status def thermostatType(self, home, rid): module_id = None for key in self.home_data.rooms[home]: if key == rid: - for module_id in self.home_data.rooms[home][rid]['module_ids']: + for module_id in self.home_data.rooms[home][rid]["module_ids"]: self.module_id = module_id if module_id in self.thermostats: return "NATherm1" @@ -264,8 +260,8 @@ def setThermmode(self, home_id, mode): postParams = { "access_token": self.getAuthToken, "home_id": home_id, - "mode": mode - } + "mode": mode, + } resp = postRequest(_SETTHERMMODE_REQ, postParams) print(resp) @@ -275,9 +271,9 @@ def setroomThermpoint(self, home_id, room_id, mode, temp=None): "home_id": home_id, "room_id": room_id, "mode": mode, - } + } if temp is not None: - postParams['temp'] = temp + postParams["temp"] = temp return postRequest(_SETROOMTHERMPOINT_REQ, postParams) @@ -288,81 +284,95 @@ class ThermostatData: Args: authData (ClientAuth): Authentication information with a working access Token """ + def __init__(self, authData): self.getAuthToken = authData.accessToken - postParams = { - "access_token" : self.getAuthToken - } + postParams = {"access_token": self.getAuthToken} resp = postRequest(_GETTHERMOSTATDATA_REQ, postParams) - self.rawData = resp['body'] - self.devList = self.rawData['devices'] - if not self.devList : raise NoDevice("No thermostat available") - self.devId = self.devList[0]['_id'] - self.modList = self.devList[0]['modules'] - self.modId = self.modList[0]['_id'] - self.temp = self.modList[0]['measured']['temperature'] - self.setpoint_mode = self.modList[0]['setpoint']['setpoint_mode'] - if self.setpoint_mode == 'manual': - self.setpoint_temp = self.modList[0]['setpoint']['setpoint_temp'] + self.rawData = resp["body"] + self.devList = self.rawData["devices"] + if not self.devList: + raise NoDevice("No thermostat available") + self.devId = self.devList[0]["_id"] + self.modList = self.devList[0]["modules"] + self.modId = self.modList[0]["_id"] + self.temp = self.modList[0]["measured"]["temperature"] + self.setpoint_mode = self.modList[0]["setpoint"]["setpoint_mode"] + if self.setpoint_mode == "manual": + self.setpoint_temp = self.modList[0]["setpoint"]["setpoint_temp"] else: - self.setpoint_temp = self.modList[0]['measured']['setpoint_temp'] - self.relay_cmd = int(self.modList[0]['therm_relay_cmd']) - self.devices = { d['_id'] : d for d in self.rawData['devices'] } + self.setpoint_temp = self.modList[0]["measured"]["setpoint_temp"] + self.relay_cmd = int(self.modList[0]["therm_relay_cmd"]) + self.devices = {d["_id"]: d for d in self.rawData["devices"]} self.modules = dict() self.therm_program_list = dict() self.zones = dict() self.timetable = dict() - for i in range(len(self.rawData['devices'])): - nameDevice=self.rawData['devices'][i]['station_name'] + for i in range(len(self.rawData["devices"])): + nameDevice = self.rawData["devices"][i]["station_name"] if nameDevice not in self.modules: - self.modules[nameDevice]=dict() - for m in self.rawData['devices'][i]['modules']: - self.modules[nameDevice][ m['_id'] ] = m - for p in self.rawData['devices'][i]['modules'][0]['therm_program_list']: - self.therm_program_list[p['program_id']] = p - for z in self.rawData['devices'][i]['modules'][0]['therm_program_list'][0]['zones']: - self.zones[z['id']] = z - for o in self.rawData['devices'][i]['modules'][0]['therm_program_list'][0]['timetable']: - self.timetable[o['m_offset']] = o - self.default_device = list(self.devices.values())[0]['station_name'] - - self.default_module = list(self.modules[self.default_device].values())[0]['module_name'] + self.modules[nameDevice] = dict() + for m in self.rawData["devices"][i]["modules"]: + self.modules[nameDevice][m["_id"]] = m + for p in self.rawData["devices"][i]["modules"][0]["therm_program_list"]: + self.therm_program_list[p["program_id"]] = p + for z in self.rawData["devices"][i]["modules"][0]["therm_program_list"][0][ + "zones" + ]: + self.zones[z["id"]] = z + for o in self.rawData["devices"][i]["modules"][0]["therm_program_list"][0][ + "timetable" + ]: + self.timetable[o["m_offset"]] = o + self.default_device = list(self.devices.values())[0]["station_name"] + + self.default_module = list(self.modules[self.default_device].values())[0][ + "module_name" + ] def lastData(self, device=None, exclude=0): s = self.deviceByName(device) - if not s : return None + if not s: + return None lastD = dict() zones = dict() # Define oldest acceptable sensor measure event limit = (time.time() - exclude) if exclude else 0 - dm = s['modules'][0]['measured'] - ds = s['modules'][0]['setpoint']['setpoint_mode'] - dz = s['modules'][0]['therm_program_list'][0]['zones'] - for module in s['modules']: - dm = module['measured'] - ds = module['setpoint']['setpoint_mode'] - dz = module['therm_program_list'][0]['zones'] - if dm['time'] > limit : - lastD[module['module_name']] = dm.copy() - lastD[module['module_name']]['setpoint_mode'] = ds + dm = s["modules"][0]["measured"] + ds = s["modules"][0]["setpoint"]["setpoint_mode"] + dz = s["modules"][0]["therm_program_list"][0]["zones"] + for module in s["modules"]: + dm = module["measured"] + ds = module["setpoint"]["setpoint_mode"] + dz = module["therm_program_list"][0]["zones"] + if dm["time"] > limit: + lastD[module["module_name"]] = dm.copy() + lastD[module["module_name"]]["setpoint_mode"] = ds # For potential use, add battery and radio coverage information to module data if present - for i in ('battery_vp', 'rf_status', 'therm_relay_cmd', 'battery_percent') : - if i in module : lastD[module['module_name']][i] = module[i] - zones[module['module_name']] = dz.copy() + for i in ( + "battery_vp", + "rf_status", + "therm_relay_cmd", + "battery_percent", + ): + if i in module: + lastD[module["module_name"]][i] = module[i] + zones[module["module_name"]] = dz.copy() return lastD def deviceById(self, did): return None if did not in self.devices else self.devices[did] def deviceByName(self, device): - if not device: device = self.default_device - for key,value in self.devices.items(): - if value['station_name'] == device: + if not device: + device = self.default_device + for key, value in self.devices.items(): + if value["station_name"] == device: return self.devices[key] def moduleById(self, mid): - for device,mod in self.modules.items(): + for device, mod in self.modules.items(): if mid in self.modules[device]: return self.modules[device][mid] return None @@ -374,12 +384,12 @@ def moduleByName(self, module=None, device=None): if device not in self.modules: return None for mod_id in self.modules[device]: - if self.modules[device][mod_id]['module_name'] == module: + if self.modules[device][mod_id]["module_name"] == module: return self.modules[device][mod_id] elif not device and module: for device, mod_ids in self.modules.items(): for mod_id in mod_ids: - if self.modules[device][mod_id]['module_name'] == module: + if self.modules[device][mod_id]["module_name"] == module: return self.modules[device][mod_id] else: return list(self.modules[device].values())[0] @@ -387,10 +397,10 @@ def moduleByName(self, module=None, device=None): def setthermpoint(self, mode, temp, endTimeOffset): postParams = {"access_token": self.getAuthToken} - postParams['device_id'] = self.devId - postParams['module_id'] = self.modId - postParams['setpoint_mode'] = mode + postParams["device_id"] = self.devId + postParams["module_id"] = self.modId + postParams["setpoint_mode"] = mode if mode == "manual": - postParams['setpoint_endtime'] = time.time() + endTimeOffset - postParams['setpoint_temp'] = temp + postParams["setpoint_endtime"] = time.time() + endTimeOffset + postParams["setpoint_temp"] = temp return postRequest(_SETTEMP_REQ, postParams) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index fbbbcc2bb..260b0c825 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -8,46 +8,48 @@ _GETMEASURE_REQ = _BASE_URL + "api/getmeasure" _GETSTATIONDATA_REQ = _BASE_URL + "api/getstationsdata" + class WeatherStationData: """ List the Weather Station devices (stations and modules) Args: authData (ClientAuth): Authentication information with a working access Token """ + def __init__(self, authData, urlReq=None): self.urlReq = urlReq or _GETSTATIONDATA_REQ self.getAuthToken = authData.accessToken - postParams = { - "access_token": self.getAuthToken - } + postParams = {"access_token": self.getAuthToken} resp = postRequest(self.urlReq, postParams) - self.rawData = resp['body']['devices'] - if not self.rawData : raise NoDevice("No weather station available") - self.stations = { d['_id'] : d for d in self.rawData } + self.rawData = resp["body"]["devices"] + if not self.rawData: + raise NoDevice("No weather station available") + self.stations = {d["_id"]: d for d in self.rawData} self.modules = dict() for i in range(len(self.rawData)): - if 'modules' not in self.rawData[i]: - self.rawData[i]['modules'] = [ self.rawData[i] ] - for m in self.rawData[i]['modules']: - if 'module_name' not in m: + if "modules" not in self.rawData[i]: + self.rawData[i]["modules"] = [self.rawData[i]] + for m in self.rawData[i]["modules"]: + if "module_name" not in m: continue - self.modules[ m['_id'] ] = m - self.modules[ m['_id'] ][ 'main_device' ] = self.rawData[i]['_id'] - self.default_station = list(self.stations.values())[0]['station_name'] + self.modules[m["_id"]] = m + self.modules[m["_id"]]["main_device"] = self.rawData[i]["_id"] + self.default_station = list(self.stations.values())[0]["station_name"] def modulesNamesList(self, station=None): - res = set([m['module_name'] for m in self.modules.values()]) + res = set([m["module_name"] for m in self.modules.values()]) if station: - res.add(self.stationByName(station)['module_name']) + res.add(self.stationByName(station)["module_name"]) else: - for id,station in self.stations.items(): - res.add(station['module_name']) + for id, station in self.stations.items(): + res.add(station["module_name"]) return list(res) def stationByName(self, station=None): - if not station : station = self.default_station - for i,s in self.stations.items(): - if s['station_name'] == station : + 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 @@ -56,27 +58,29 @@ def stationById(self, sid): def moduleByName(self, module, station=None): s = None - if station : + if station: s = self.stationByName(station) - if not s : return None - elif s['module_name'] == module: + if not s: + return None + elif s["module_name"] == module: return s else: for id, station in self.stations.items(): - if 'module_name' in station and station['module_name'] == module: + if "module_name" in station and station["module_name"] == module: return station 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 + if mod["module_name"] == module: + if not s or mod["main_device"] == s["_id"]: + return mod return None def moduleById(self, mid, sid=None): s = self.stationById(sid) if sid else None - if mid in self.modules : + if mid in self.modules: if s: - for module in s['modules']: - if module['_id'] == mid: + for module in s["modules"]: + if module["_id"] == mid: return module else: return self.modules[mid] @@ -84,41 +88,47 @@ def moduleById(self, mid, sid=None): def monitoredConditions(self, module): mod = self.moduleByName(module) conditions = [] - for cond in mod['data_type']: - if cond == 'Wind': + for cond in mod["data_type"]: + if cond == "Wind": # the Wind meter actually exposes the following conditions - conditions.extend(['windangle', 'windstrength', 'gustangle', 'guststrength']) + conditions.extend( + ["windangle", "windstrength", "gustangle", "guststrength"] + ) else: conditions.append(cond.lower()) - if mod['type'] == 'NAMain': + if mod["type"] == "NAMain": # the main module has wifi_status - conditions.append('wifi_status') + conditions.append("wifi_status") else: # assume all other modules have rf_status, battery_vp, and battery_percent - conditions.extend(['rf_status', 'battery_vp', 'battery_percent']) + conditions.extend(["rf_status", "battery_vp", "battery_percent"]) return conditions def lastData(self, station=None, exclude=0): s = self.stationByName(station) - if not s : return None + if not s: + return None lastD = dict() # Define oldest acceptable sensor measure event limit = (time.time() - exclude) if exclude else 0 - ds = s['dashboard_data'] - if 'module_name' in s and ds['time_utc'] > limit : - lastD[s['module_name']] = ds.copy() - lastD[s['module_name']]['When'] = lastD[s['module_name']].pop("time_utc") - lastD[s['module_name']]['wifi_status'] = s['wifi_status'] + ds = s["dashboard_data"] + if "module_name" in s and ds["time_utc"] > limit: + lastD[s["module_name"]] = ds.copy() + lastD[s["module_name"]]["When"] = lastD[s["module_name"]].pop("time_utc") + lastD[s["module_name"]]["wifi_status"] = s["wifi_status"] for module in s["modules"]: - if 'dashboard_data' not in module: + if "dashboard_data" not in module: continue - ds = module['dashboard_data'] - if 'time_utc' in ds and ds['time_utc'] > limit : - lastD[module['module_name']] = ds.copy() - lastD[module['module_name']]['When'] = lastD[module['module_name']].pop("time_utc") + ds = module["dashboard_data"] + if "time_utc" in ds and ds["time_utc"] > limit: + lastD[module["module_name"]] = ds.copy() + lastD[module["module_name"]]["When"] = lastD[module["module_name"]].pop( + "time_utc" + ) # For potential use, add battery and radio coverage information to module data if present - for i in ('rf_status', 'battery_vp', 'battery_percent') : - if i in module : lastD[module['module_name']][i] = module[i] + for i in ("rf_status", "battery_vp", "battery_percent"): + if i in module: + lastD[module["module_name"]][i] = module[i] else: return None return lastD @@ -126,72 +136,97 @@ def lastData(self, station=None, exclude=0): def checkNotUpdated(self, station=None, delay=3600): res = self.lastData(station) ret = [] - for mn,v in res.items(): - if time.time()-v['When'] > delay : ret.append(mn) + for mn, v in res.items(): + if time.time() - v["When"] > delay: + ret.append(mn) return ret if ret else None def checkUpdated(self, station=None, delay=3600): res = self.lastData(station) ret = [] - for mn,v in res.items(): - if time.time()-v['When'] < delay : ret.append(mn) + for mn, v in res.items(): + if time.time() - v["When"] < delay: + ret.append(mn) return ret if ret else None - def getMeasure(self, device_id, scale, mtype, module_id=None, date_begin=None, date_end=None, limit=None, optimize=False, real_time=False): - postParams = { "access_token" : self.getAuthToken } - postParams['device_id'] = device_id - if module_id : postParams['module_id'] = module_id - postParams['scale'] = scale - postParams['type'] = mtype - if date_begin : postParams['date_begin'] = date_begin - if date_end : postParams['date_end'] = date_end - if limit : postParams['limit'] = limit - postParams['optimize'] = "true" if optimize else "false" - postParams['real_time'] = "true" if real_time else "false" + def getMeasure( + self, + device_id, + scale, + mtype, + module_id=None, + date_begin=None, + date_end=None, + limit=None, + optimize=False, + real_time=False, + ): + postParams = {"access_token": self.getAuthToken} + postParams["device_id"] = device_id + if module_id: + postParams["module_id"] = module_id + postParams["scale"] = scale + postParams["type"] = mtype + if date_begin: + postParams["date_begin"] = date_begin + if date_end: + postParams["date_end"] = date_end + if limit: + postParams["limit"] = limit + postParams["optimize"] = "true" if optimize else "false" + postParams["real_time"] = "true" if real_time else "false" return postRequest(_GETMEASURE_REQ, postParams) def MinMaxTH(self, station=None, module=None, frame="last24"): - if not station : station = self.default_station + if not station: + station = self.default_station s = self.stationByName(station) - if not s : + if not s: s = self.stationById(station) - if not s : return None + if not s: + return None if frame == "last24": end = time.time() - start = end - 24*3600 # 24 hours ago + start = end - 24 * 3600 # 24 hours ago elif frame == "day": start, end = todayStamps() - if module and module != s['module_name']: - m = self.moduleByName(module, s['station_name']) - if not m : - m = self.moduleById(s['_id'], module) - if not m : return None + if module and module != s["module_name"]: + m = self.moduleByName(module, s["station_name"]) + if not m: + m = self.moduleById(s["_id"], module) + if not m: + return None # retrieve module's data resp = self.getMeasure( - device_id = s['_id'], - module_id = m['_id'], - scale = "max", - mtype = "Temperature,Humidity", - date_begin = start, - date_end = end) - else : # retrieve station's data + device_id=s["_id"], + module_id=m["_id"], + scale="max", + mtype="Temperature,Humidity", + date_begin=start, + date_end=end, + ) + else: # retrieve station's data resp = self.getMeasure( - device_id = s['_id'], - scale = "max", - mtype = "Temperature,Humidity", - date_begin = start, - date_end = end) + device_id=s["_id"], + scale="max", + mtype="Temperature,Humidity", + date_begin=start, + date_end=end, + ) if resp: - T = [v[0] for v in resp['body'].values()] - H = [v[1] for v in resp['body'].values()] + T = [v[0] for v in resp["body"].values()] + H = [v[1] for v in resp["body"].values()] return min(T), max(T), min(H), max(H) else: return None + class DeviceList(WeatherStationData): """ This class is now deprecated. Use WeatherStationData directly instead """ - warnings.warn("The 'DeviceList' class was renamed 'WeatherStationData'", - DeprecationWarning ) + + warnings.warn( + "The 'DeviceList' class was renamed 'WeatherStationData'", DeprecationWarning + ) pass diff --git a/smart_home/__init__.py b/smart_home/__init__.py index ed91b8117..b8c2c5e1c 100644 --- a/smart_home/__init__.py +++ b/smart_home/__init__.py @@ -4,19 +4,20 @@ import json # Common definitions -_BASE_URL = "https://api.netatmo.com/" +_BASE_URL = "https://api.netatmo.com/" # HTTP libraries depends upon Python 2 or 3 -if version_info.major == 3 : +if version_info.major == 3: import urllib.parse, urllib.request else: from urllib import urlencode import urllib2 -class NoDevice( Exception ): +class NoDevice(Exception): pass + # Utilities routines @@ -24,27 +25,48 @@ def postRequest(url, params=None, timeout=10): if version_info.major == 3: req = urllib.request.Request(url) if params: - req.add_header("Content-Type","application/x-www-form-urlencoded;charset=utf-8") - params = urllib.parse.urlencode(params).encode('utf-8') + req.add_header( + "Content-Type", "application/x-www-form-urlencoded;charset=utf-8" + ) + params = urllib.parse.urlencode(params).encode("utf-8") try: - resp = urllib.request.urlopen(req, params, timeout=timeout) if params else urllib.request.urlopen(req, timeout=timeout) + resp = ( + urllib.request.urlopen(req, params, timeout=timeout) + if params + else urllib.request.urlopen(req, timeout=timeout) + ) except urllib.error.URLError: print(traceback.format_exc()) return None else: if params: params = urlencode(params) - headers = {"Content-Type" : "application/x-www-form-urlencoded;charset=utf-8"} - req = urllib2.Request(url=url, data=params, headers=headers) if params else urllib2.Request(url) + headers = { + "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" + } + req = ( + urllib2.Request(url=url, data=params, headers=headers) + if params + else urllib2.Request(url) + ) try: resp = urllib2.urlopen(req, timeout=timeout) except urllib2.URLError: return None data = b"" - for buff in iter(lambda: resp.read(65535), b''): data += buff + for buff in iter(lambda: resp.read(65535), b""): + data += buff # Return values in bytes if not json data to handle properly camera images - returnedContentType = resp.getheader("Content-Type") if version_info.major == 3 else resp.info()["Content-Type"] - return json.loads(data.decode("utf-8")) if "application/json" in returnedContentType else data + returnedContentType = ( + resp.getheader("Content-Type") + if version_info.major == 3 + else resp.info()["Content-Type"] + ) + return ( + json.loads(data.decode("utf-8")) + if "application/json" in returnedContentType + else data + ) def toTimeString(value): @@ -52,13 +74,14 @@ def toTimeString(value): def toEpoch(value): - return int(time.mktime(time.strptime(value,"%Y-%m-%d_%H:%M:%S"))) + return int(time.mktime(time.strptime(value, "%Y-%m-%d_%H:%M:%S"))) def todayStamps(): today = time.strftime("%Y-%m-%d") - today = int(time.mktime(time.strptime(today,"%Y-%m-%d"))) - return today, today+3600*24 + today = int(time.mktime(time.strptime(today, "%Y-%m-%d"))) + return today, today + 3600 * 24 + # Global shortcut @@ -66,22 +89,27 @@ def todayStamps(): def getStationMinMaxTH(station=None, module=None): from pyatmo import ClientAuth from smart_home.WeatherStation import DeviceList + authorization = ClientAuth() devList = DeviceList(authorization) - if not station : station = devList.default_station - if module : + if not station: + station = devList.default_station + if module: mname = module - else : - mname = devList.stationByName(station)['module_name'] + else: + mname = devList.stationByName(station)["module_name"] lastD = devList.lastData(station) if mname == "*": result = dict() for m in lastD.keys(): - if time.time()-lastD[m]['When'] > 3600 : continue + if time.time() - lastD[m]["When"] > 3600: + continue r = devList.MinMaxTH(module=m) - result[m] = (r[0], lastD[m]['Temperature'], r[1]) + result[m] = (r[0], lastD[m]["Temperature"], r[1]) else: - if time.time()-lastD[mname]['When'] > 3600 : result = ["-", "-"] - else : result = [lastD[mname]['Temperature'], lastD[mname]['Humidity']] + if time.time() - lastD[mname]["When"] > 3600: + result = ["-", "-"] + else: + result = [lastD[mname]["Temperature"], lastD[mname]["Humidity"]] result.extend(devList.MinMaxTH(station, mname)) return result From 0bc90150cefaee77bc3d21c9fd95a5957b14f804 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Tue, 8 Jan 2019 23:30:37 +0100 Subject: [PATCH 035/333] v1.6 Signed-off-by: Hugo D. (jabesq) --- README.md | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b6bdeec9..cd758cef6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ netatmo-api-python ================== +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) Simple API to access Netatmo weather station data from any python script For more detailed information see http://dev.netatmo.com diff --git a/setup.py b/setup.py index 11823b71f..1d6fa3c79 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="1.5", # Should be updated with new versions + version="1.6", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From 56fadce374d2829924adccd184d2ec723427a9b3 Mon Sep 17 00:00:00 2001 From: Baosong Shan Date: Thu, 17 Jan 2019 18:45:52 +0100 Subject: [PATCH 036/333] Fix crashing when response of gethomestatus not correct --- smart_home/Thermostat.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 15161d964..2c93ece65 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -13,6 +13,7 @@ _SETROOMTHERMPOINT_REQ = _BASE_URL + "api/setroomthermpoint" _GETROOMMEASURE_REQ = _BASE_URL + "api/getroommeasure" +_MAX_RETRIES = 10 class HomeData: """ @@ -117,6 +118,9 @@ def __init__(self, authData, home_id=None, home=None): postParams = {"access_token": self.getAuthToken, "home_id": self.home_id} resp = postRequest(_GETHOMESTATUS_REQ, postParams) + if "body" in resp and "errors" in resp["body"] or "body" not in resp or "home" not in resp["body"]: + raise NoDevice("No device found, errors in response") + return None self.rawData = resp["body"]["home"] self.rooms = dict() self.thermostats = dict() From 5298f410710b6f85fd5d46f4d2f1bf1ba140030a Mon Sep 17 00:00:00 2001 From: Baosong Shan Date: Thu, 17 Jan 2019 18:48:33 +0100 Subject: [PATCH 037/333] Fix crashing when response of gethomestatus not correct --- smart_home/Thermostat.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 2c93ece65..5b46e0bb6 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -13,8 +13,6 @@ _SETROOMTHERMPOINT_REQ = _BASE_URL + "api/setroomthermpoint" _GETROOMMEASURE_REQ = _BASE_URL + "api/getroommeasure" -_MAX_RETRIES = 10 - class HomeData: """ List the Energy devices (relays, thermostat modules and valves) From 74783e18b8cd2ee069172f0c8210f199826ff3cd Mon Sep 17 00:00:00 2001 From: Baosong Shan Date: Thu, 17 Jan 2019 18:49:15 +0100 Subject: [PATCH 038/333] Fix crashing when response of gethomestatus not correct --- smart_home/Thermostat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 5b46e0bb6..679887388 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -13,6 +13,7 @@ _SETROOMTHERMPOINT_REQ = _BASE_URL + "api/setroomthermpoint" _GETROOMMEASURE_REQ = _BASE_URL + "api/getroommeasure" + class HomeData: """ List the Energy devices (relays, thermostat modules and valves) From 0f6500f784280a53849382367f305703358b870e Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Sat, 19 Jan 2019 17:28:46 +0100 Subject: [PATCH 039/333] Fix Breaking change from Netatmo: dashboard_data no longer available if station lost please see: https://github.com/philippelt/netatmo-api-python/commit/5a706ba643cff4583907a53aa155ada93ca995a2#diff-690f0bad212f5270ce60e24e3cf0859b --- smart_home/WeatherStation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 260b0c825..1a5a8399f 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -106,7 +106,8 @@ def monitoredConditions(self, module): def lastData(self, station=None, exclude=0): s = self.stationByName(station) - if not s: + # Breaking change from Netatmo : dashboard_data no longer available if station lost + if not s or 'dashboard_data' not in s: return None lastD = dict() # Define oldest acceptable sensor measure event From 5995bdde0238ff4d2c89a5993589c6f617bd2276 Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Sat, 19 Jan 2019 17:46:43 +0100 Subject: [PATCH 040/333] Fix of TypeError: 'NoneType' object is not iterable --- smart_home/WeatherStation.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 1a5a8399f..b290f2b79 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -130,8 +130,6 @@ def lastData(self, station=None, exclude=0): for i in ("rf_status", "battery_vp", "battery_percent"): if i in module: lastD[module["module_name"]][i] = module[i] - else: - return None return lastD def checkNotUpdated(self, station=None, delay=3600): From 194214494c137858ac33cb5438838625376062c9 Mon Sep 17 00:00:00 2001 From: Michael Svinth Date: Sun, 20 Jan 2019 15:46:47 +0100 Subject: [PATCH 041/333] Fix/improve support for Health Coach --- smart_home/WeatherStation.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index b290f2b79..0cf686901 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -31,7 +31,10 @@ def __init__(self, authData, urlReq=None): self.rawData[i]["modules"] = [self.rawData[i]] for m in self.rawData[i]["modules"]: if "module_name" not in m: - continue + if m["type"] == "NHC": + m["module_name"] = m["station_name"] + else: + continue self.modules[m["_id"]] = m self.modules[m["_id"]]["main_device"] = self.rawData[i]["_id"] self.default_station = list(self.stations.values())[0]["station_name"] @@ -96,7 +99,7 @@ def monitoredConditions(self, module): ) else: conditions.append(cond.lower()) - if mod["type"] == "NAMain": + if mod["type"] == "NAMain" or mod["type"] == "NHC": # the main module has wifi_status conditions.append("wifi_status") else: @@ -127,7 +130,7 @@ def lastData(self, station=None, exclude=0): "time_utc" ) # For potential use, add battery and radio coverage information to module data if present - for i in ("rf_status", "battery_vp", "battery_percent"): + for i in ("rf_status", "battery_vp", "battery_percent", "wifi_status"): if i in module: lastD[module["module_name"]][i] = module[i] return lastD From b8a135cbfcf31b31317fb14b7ffc82a2352fe6cb Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 20 Jan 2019 18:17:37 +0100 Subject: [PATCH 042/333] Black formatting fix Signed-off-by: Hugo D. (jabesq) --- smart_home/Thermostat.py | 7 ++++++- smart_home/WeatherStation.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 679887388..08ec012a9 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -117,7 +117,12 @@ def __init__(self, authData, home_id=None, home=None): postParams = {"access_token": self.getAuthToken, "home_id": self.home_id} resp = postRequest(_GETHOMESTATUS_REQ, postParams) - if "body" in resp and "errors" in resp["body"] or "body" not in resp or "home" not in resp["body"]: + if ( + "body" in resp + and "errors" in resp["body"] + or "body" not in resp + or "home" not in resp["body"] + ): raise NoDevice("No device found, errors in response") return None self.rawData = resp["body"]["home"] diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 0cf686901..42f219dfc 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -110,7 +110,7 @@ def monitoredConditions(self, module): def lastData(self, station=None, exclude=0): s = self.stationByName(station) # Breaking change from Netatmo : dashboard_data no longer available if station lost - if not s or 'dashboard_data' not in s: + if not s or "dashboard_data" not in s: return None lastD = dict() # Define oldest acceptable sensor measure event From 26f2d0e4685de6229d8f1949bb4af46ec443d8a2 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 20 Jan 2019 18:19:50 +0100 Subject: [PATCH 043/333] v1.7 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1d6fa3c79..c5272e748 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="1.6", # Should be updated with new versions + version="1.7", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From 51fd36637a77239eb1a92e3bf430c7f2d5511a57 Mon Sep 17 00:00:00 2001 From: Lasse Valentini Jensen Date: Sun, 20 Jan 2019 21:18:18 +0100 Subject: [PATCH 044/333] Ignores data from unnamed extra modules --- smart_home/WeatherStation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 42f219dfc..631bbf507 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -121,7 +121,7 @@ def lastData(self, station=None, exclude=0): lastD[s["module_name"]]["When"] = lastD[s["module_name"]].pop("time_utc") lastD[s["module_name"]]["wifi_status"] = s["wifi_status"] for module in s["modules"]: - if "dashboard_data" not in module: + if "dashboard_data" not in module or "module_name" not in module: continue ds = module["dashboard_data"] if "time_utc" in ds and ds["time_utc"] > limit: From 9aeb5efd559a15b8884da7b28c2dcce94f8655c9 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sun, 3 Feb 2019 13:32:26 +0100 Subject: [PATCH 045/333] Added webhook methods --- pyatmo.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pyatmo.py b/pyatmo.py index ea3a4777f..f154e447f 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -11,6 +11,7 @@ coding=utf-8 """ import time +import logging from smart_home.WeatherStation import WeatherStationData, DeviceList from smart_home.Camera import CameraData @@ -19,6 +20,8 @@ from smart_home.HomeCoach import HomeCoachData from smart_home import _BASE_URL, postRequest, NoDevice +LOG = logging.getLogger(__name__) + ######################## USER SPECIFIC INFORMATION ###################### # To be able to have a program accessing your netatmo data, you have to register your program as @@ -37,6 +40,8 @@ # Common definitions _AUTH_REQ = _BASE_URL + "oauth2/token" +_WEBHOOK_URL_ADD = _BASE_URL + "api/addwebhook" +_WEBHOOK_URL_DROP = _BASE_URL + "api/dropwebhook" class ClientAuth: @@ -82,6 +87,23 @@ def __init__( self._scope = resp["scope"] self.expiration = int(resp["expire_in"] + time.time() - 1800) + def addwebhook(self, webhook_url): + postParams = { + "access_token": self._accessToken, + "url": webhook_url, + "app_types": "app_security" + } + resp = postRequest(_WEBHOOK_URL_ADD, postParams) + LOG.debug("addwebhook: %s", resp) + + def dropwebhook(self): + postParams = { + "access_token": self._accessToken, + "app_types": "app_security" + } + resp = postRequest(_WEBHOOK_URL_DROP, postParams) + LOG.debug("dropwebhook: %s", resp) + @property def accessToken(self): From 0f4ac3eb0a369b485376360623e63b80ce212256 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 3 Feb 2019 21:06:09 +0100 Subject: [PATCH 046/333] Black coding style Signed-off-by: Hugo D. (jabesq) --- pyatmo.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyatmo.py b/pyatmo.py index f154e447f..ef0b8ee69 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -91,16 +91,13 @@ def addwebhook(self, webhook_url): postParams = { "access_token": self._accessToken, "url": webhook_url, - "app_types": "app_security" + "app_types": "app_security", } resp = postRequest(_WEBHOOK_URL_ADD, postParams) LOG.debug("addwebhook: %s", resp) def dropwebhook(self): - postParams = { - "access_token": self._accessToken, - "app_types": "app_security" - } + postParams = {"access_token": self._accessToken, "app_types": "app_security"} resp = postRequest(_WEBHOOK_URL_DROP, postParams) LOG.debug("dropwebhook: %s", resp) From 490f5911a02f9a0bbea7cf62616fee304839c4f8 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 3 Feb 2019 21:07:15 +0100 Subject: [PATCH 047/333] v1.8 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c5272e748..d4cc7b36f 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="1.7", # Should be updated with new versions + version="1.8", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From 98b1b22512e9c634f3da8065ec5621b299d9d901 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 11 Mar 2019 21:22:03 +0100 Subject: [PATCH 048/333] Add usage of shell environment variables for credentials (#39) --- pyatmo.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/pyatmo.py b/pyatmo.py index ef0b8ee69..317e8cc83 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -139,13 +139,30 @@ def __init__(self, authData): from sys import exit, stdout, stderr + try: + import os + if (os.environ["CLIENT_ID"] and os.environ["CLIENT_SECRET"] and + os.environ["USERNAME"] and os.environ["PASSWORD"]): + _CLIENT_ID = os.environ["CLIENT_ID"] + _CLIENT_SECRET = os.environ["CLIENT_SECRET"] + _USERNAME = os.environ["USERNAME"] + _PASSWORD = os.environ["PASSWORD"] + except KeyError: + stderr.write( + "No credentials passed to pyatmo.py (client_id, client_secret, username, password)\n" + ) + if not _CLIENT_ID or not _CLIENT_SECRET or not _USERNAME or not _PASSWORD: stderr.write( - "Library source missing identification arguments to check lnetatmo.py (user/password/etc...)" + "Library source missing identification arguments to check pyatmo.py (user/password/etc...)\n" ) exit(1) authorization = ClientAuth( + clientId = _CLIENT_ID, + clientSecret = _CLIENT_SECRET, + username = _USERNAME, + password = _PASSWORD, scope="read_station read_camera access_camera read_thermostat write_thermostat read_presence access_presence" ) # Test authentication method @@ -153,7 +170,7 @@ def __init__(self, authData): devList = DeviceList(authorization) # Test DEVICELIST except NoDevice: if stdout.isatty(): - print("lnetatmo.py : warning, no weather station available for testing") + print("pyatmo.py : warning, no weather station available for testing") else: devList.MinMaxTH() # Test GETMEASUR @@ -161,13 +178,13 @@ def __init__(self, authData): Camera = CameraData(authorization) except NoDevice: if stdout.isatty(): - print("lnetatmo.py : warning, no camera available for testing") + print("pyatmo.py : warning, no camera available for testing") try: Thermostat = ThermostatData(authorization) except NoDevice: if stdout.isatty(): - print("lnetatmo.py : warning, no thermostat available for testing") + print("pyatmo.py : warning, no thermostat available for testing") PublicData(authorization) @@ -175,6 +192,6 @@ def __init__(self, authData): # If launched interactively, display OK message if stdout.isatty(): - print("lnetatmo.py : OK") + print("pyatmo.py : OK") exit(0) From bb714189868bb5235ad4d1241dd4818fe633fe70 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 11 Mar 2019 21:22:48 +0100 Subject: [PATCH 049/333] Update README.md (#38) --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cd758cef6..5f4d6dfdb 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,23 @@ netatmo-api-python ================== [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) -Simple API to access Netatmo weather station data from any python script +Simple API to access Netatmo devices and data like weather station or camera data from python. For more detailed information see http://dev.netatmo.com -I have no relation with the netatmo company, I wrote this because I needed it myself, -and published it to save time to anyone who would have same needs. +This project has no relation with the Netatmo company. ### Install ### -To install lnetatmo simply run: +To install pyatmo simply run: - python setup.py install + pip install pyatmo Depending on your permissions you might be required to use sudo. -Once installed you can simple add lnetatmo to your python scripts by including: +Once installed you can simple add `pyatmo` to your python scripts by including: - import lnetatmo + import pyatmo ### Note ### -this release is an interim release waiting for Netatmo dashboard finalization. As any work in progress, it can stop to work at any time if serious changes occurs. +The module requires a valid user account and a registered application. See usage.md for further information. +Be aware that the module may stop working if Netatmo decides to change their API. From 1db4f4a4137789a1ce2f04bd8774ff867c2c8007 Mon Sep 17 00:00:00 2001 From: shanbs Date: Wed, 13 Mar 2019 23:04:40 +0100 Subject: [PATCH 050/333] Fix the crash due to absence of the "name" of therm_schedules (#40) --- smart_home/Thermostat.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 08ec012a9..b08535125 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -63,11 +63,11 @@ def __init__(self, authData): for s in self.rawData[i]["therm_schedules"]: self.schedules[nameHome][s["id"]] = s for t in range(len(self.rawData[i]["therm_schedules"])): - nameSchedule = self.rawData[i]["therm_schedules"][t]["name"] - if nameSchedule not in self.zones[nameHome]: - self.zones[nameHome][nameSchedule] = dict() + idSchedule = self.rawData[i]["therm_schedules"][t]["id"] + if idSchedule not in self.zones[nameHome]: + self.zones[nameHome][idSchedule] = dict() for z in self.rawData[i]["therm_schedules"][t]["zones"]: - self.zones[nameHome][nameSchedule][z["id"]] = z + self.zones[nameHome][idSchedule][z["id"]] = z def homeById(self, hid): return None if hid not in self.homes else self.homes[hid] From f9727edb51a5208d52bfa4e558a31e2978e2e007 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 19 Mar 2019 11:30:36 +0100 Subject: [PATCH 051/333] Comment out print statements (#41) This should be either logged correctly or just be discarded. --- smart_home/Thermostat.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index b08535125..6ab3fae0f 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -157,7 +157,7 @@ def __init__(self, authData, home_id=None, home=None): self.default_relay = list(self.relays.values())[0] if self.thermostats != {}: self.default_thermostat = list(self.thermostats.values())[0] - print(self.thermostats) + # print(self.thermostats) if self.valves != {}: self.default_valve = list(self.valves.values())[0] @@ -233,7 +233,7 @@ def measuredTemperature(self, rid=None): Return the measured temperature of a given room. """ temperature = None - print(rid) + # print(rid) if rid: room_data = self.roomById(rid=rid) else: @@ -244,7 +244,7 @@ def measuredTemperature(self, rid=None): def boilerStatus(self, rid=None): boiler_status = None - print(rid) + # print(rid) if rid: relay_status = self.thermostatById(rid=rid) else: @@ -271,7 +271,7 @@ def setThermmode(self, home_id, mode): "mode": mode, } resp = postRequest(_SETTHERMMODE_REQ, postParams) - print(resp) + # print(resp) def setroomThermpoint(self, home_id, room_id, mode, temp=None): postParams = { From 5a438a6467f6dab56af4d6bf421a48f75515bc28 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sun, 24 Mar 2019 21:41:25 +0100 Subject: [PATCH 052/333] Use pyatmo instead of lnetatmo (#43) --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2ac44a943..4ac39cd6d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ build/ dist/ -lnetatmo.egg-info/ +pyatmo.egg-info/ *.pyc .DS_Store From 5bfd32745c8ca51a9d372501f52522b0d8836224 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sun, 24 Mar 2019 21:42:00 +0100 Subject: [PATCH 053/333] Use propper logging (#42) * Use propper logging * Revert line split * Revert false substitution --- smart_home/Camera.py | 24 ++++++++++++++---------- smart_home/Thermostat.py | 20 ++++++++++++-------- smart_home/__init__.py | 6 +++++- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index b0bef04c6..8c208bda9 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -3,10 +3,14 @@ """ import imghdr import time +import logging from urllib.error import URLError from . import NoDevice, postRequest, _BASE_URL + +LOG = logging.getLogger(__name__) + _GETHOMEDATA_REQ = _BASE_URL + "api/gethomedata" _GETCAMERAPICTURE_REQ = _BASE_URL + "api/getcamerapicture" _GETEVENTSUNTIL_REQ = _BASE_URL + "api/geteventsuntil" @@ -279,7 +283,7 @@ def personSeenByCamera(self, name, home=None, camera=None, exclude=0): try: cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: - print("personSeenByCamera: Camera name or home is unknown") + LOG.error("personSeenByCamera: Camera name or home is unknown") return False # Check in the last event is someone known has been seen if exclude: @@ -321,7 +325,7 @@ def someoneKnownSeen(self, home=None, camera=None, exclude=0): try: cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: - print("someoneKnownSeen: Camera name or home is unknown") + LOG.error("someoneKnownSeen: Camera name or home is unknown") return False if exclude: @@ -350,7 +354,7 @@ def someoneUnknownSeen(self, home=None, camera=None, exclude=0): try: cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: - print("someoneUnknownSeen: Camera name or home is unknown") + LOG.error("someoneUnknownSeen: Camera name or home is unknown") return False if exclude: @@ -379,7 +383,7 @@ def motionDetected(self, home=None, camera=None, exclude=0): try: cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: - print("motionDetected: Camera name or home is unknown") + LOG.error("motionDetected: Camera name or home is unknown") return False if exclude: @@ -402,7 +406,7 @@ def outdoormotionDetected(self, home=None, camera=None, offset=0): try: cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: - print("outdoormotionDetected: Camera name or home is unknown") + LOG.error("outdoormotionDetected: Camera name or home is unknown") return False if cam_id in self.lastEvent: if self.lastEvent[cam_id]["type"] == "movement": @@ -421,7 +425,7 @@ def humanDetected(self, home=None, camera=None, offset=0): try: cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: - print("personSeenByCamera: Camera name or home is unknown") + LOG.error("personSeenByCamera: Camera name or home is unknown") return False if self.outdoor_lastEvent[cam_id]["video_status"] == "recording": for e in self.outdoor_lastEvent[cam_id]["event_list"]: @@ -436,7 +440,7 @@ def animalDetected(self, home=None, camera=None, offset=0): try: cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: - print("animalDetected: Camera name or home is unknown") + LOG.error("animalDetected: Camera name or home is unknown") return False if self.outdoor_lastEvent[cam_id]["video_status"] == "recording": @@ -452,7 +456,7 @@ def carDetected(self, home=None, camera=None, offset=0): try: cam_id = self.cameraByName(camera=camera, home=home)["id"] except TypeError: - print("carDetected: Camera name or home is unknown") + LOG.error("carDetected: Camera name or home is unknown") return False if self.outdoor_lastEvent[cam_id]["video_status"] == "recording": @@ -470,7 +474,7 @@ def moduleMotionDetected(self, module=None, home=None, camera=None, exclude=0): mod_id = mod["id"] cam_id = mod["cam_id"] except TypeError: - print( + LOG.error( "moduleMotionDetected: Module name or" "Camera name or home is unknown" ) return False @@ -503,7 +507,7 @@ def moduleOpened(self, module=None, home=None, camera=None, exclude=0): mod_id = mod["id"] cam_id = mod["cam_id"] except TypeError: - print("moduleOpened: Camera name, or home, or module is unknown") + LOG.error("moduleOpened: Camera name, or home, or module is unknown") return False if exclude: diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 6ab3fae0f..d326e0ea2 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -2,9 +2,13 @@ coding=utf-8 """ import time +import logging from . import NoDevice, postRequest, _BASE_URL + +LOG = logging.getLogger(__name__) + _SETTEMP_REQ = _BASE_URL + "api/setthermpoint" _GETTHERMOSTATDATA_REQ = _BASE_URL + "api/getthermostatsdata" _GETHOMESDATA_REQ = _BASE_URL + "api/homesdata" @@ -84,8 +88,8 @@ def gethomeId(self, home=None): home = self.default_home for key, value in self.homes.items(): if value["name"] == home: - # print(self.homes[key]['id']) - # print(self.default_home) + LOG.debug(self.homes[key]['id']) + LOG.debug(self.default_home) if "therm_schedules" in self.homes[key]: return self.homes[key]["id"] @@ -109,7 +113,7 @@ def __init__(self, authData, home_id=None, home=None): # print(home_data.modules) if home_id: self.home_id = home_id - # print('home_id', self.home_id) + LOG.debug('home_id', self.home_id) elif home: self.home_id = self.home_data.gethomeId(home=home) else: @@ -157,7 +161,7 @@ def __init__(self, authData, home_id=None, home=None): self.default_relay = list(self.relays.values())[0] if self.thermostats != {}: self.default_thermostat = list(self.thermostats.values())[0] - # print(self.thermostats) + LOG.debug(self.thermostats) if self.valves != {}: self.default_valve = list(self.valves.values())[0] @@ -218,7 +222,7 @@ def setPointmode(self, rid=None): def getAwaytemp(self, home=None): if not home: home = self.home_data.default_home - # print(self.home_data.default_home) + LOG.debug(self.home_data.default_home) data = self.home_data.getSelectedschedule(home=home) return data["away_temp"] @@ -233,7 +237,7 @@ def measuredTemperature(self, rid=None): Return the measured temperature of a given room. """ temperature = None - # print(rid) + LOG.debug(rid) if rid: room_data = self.roomById(rid=rid) else: @@ -244,7 +248,7 @@ def measuredTemperature(self, rid=None): def boilerStatus(self, rid=None): boiler_status = None - # print(rid) + LOG.debug(rid) if rid: relay_status = self.thermostatById(rid=rid) else: @@ -271,7 +275,7 @@ def setThermmode(self, home_id, mode): "mode": mode, } resp = postRequest(_SETTHERMMODE_REQ, postParams) - # print(resp) + LOG.debug(resp) def setroomThermpoint(self, home_id, room_id, mode, temp=None): postParams = { diff --git a/smart_home/__init__.py b/smart_home/__init__.py index b8c2c5e1c..1eb412359 100644 --- a/smart_home/__init__.py +++ b/smart_home/__init__.py @@ -2,6 +2,10 @@ import traceback from sys import version_info import json +import logging + + +LOG = logging.getLogger(__name__) # Common definitions _BASE_URL = "https://api.netatmo.com/" @@ -36,7 +40,7 @@ def postRequest(url, params=None, timeout=10): else urllib.request.urlopen(req, timeout=timeout) ) except urllib.error.URLError: - print(traceback.format_exc()) + LOG.error(traceback.format_exc()) return None else: if params: From d9e6fbff443223d74ae6d0e75209a605b8535dbf Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 24 Mar 2019 21:45:11 +0100 Subject: [PATCH 054/333] v1.9 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d4cc7b36f..656e63164 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="1.8", # Should be updated with new versions + version="1.9", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From 4c471bc26fdbdc969878d164ff9b0b42f6722eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20M=C3=A5nsson?= Date: Wed, 27 Mar 2019 16:14:05 +0100 Subject: [PATCH 055/333] Adding support for sum_rain_24 and sum_rain_1 (#44) Adding support for amount of rain "today" and "the last hour". --- smart_home/WeatherStation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 631bbf507..5a036e057 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -97,6 +97,10 @@ def monitoredConditions(self, module): conditions.extend( ["windangle", "windstrength", "gustangle", "guststrength"] ) + elif cond == "Rain": + conditions.extend( + ["sum_rain_24", "sum_rain_1"] + ) else: conditions.append(cond.lower()) if mod["type"] == "NAMain" or mod["type"] == "NHC": From ec81f93045cb2291121703dc91bdfc098b9845e9 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Tue, 2 Apr 2019 22:33:16 +0200 Subject: [PATCH 056/333] v1.10 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 656e63164..e2cfbce91 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="1.9", # Should be updated with new versions + version="1.10", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From 02b7d28ff8a02d1a4ed247c773a635069a9a548a Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 8 May 2019 14:21:42 +0200 Subject: [PATCH 057/333] Add rain condition back in (#45) Add the rain condition back in as it was hidden by #44. --- smart_home/WeatherStation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 5a036e057..7c3c2c98a 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -99,7 +99,7 @@ def monitoredConditions(self, module): ) elif cond == "Rain": conditions.extend( - ["sum_rain_24", "sum_rain_1"] + ["Rain", "sum_rain_24", "sum_rain_1"] ) else: conditions.append(cond.lower()) From 70c2b2c9f8effaece5eaa899a3e68b72995f8953 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 8 May 2019 14:22:04 +0200 Subject: [PATCH 058/333] Add schedule switch function (#47) * Add schedule switch function * Add debug message * Revert changes to gethomeId --- smart_home/Thermostat.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index d326e0ea2..e3263171c 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -16,6 +16,12 @@ _SETTHERMMODE_REQ = _BASE_URL + "api/setthermmode" _SETROOMTHERMPOINT_REQ = _BASE_URL + "api/setroomthermpoint" _GETROOMMEASURE_REQ = _BASE_URL + "api/getroommeasure" +_SWITCHHOMESCHEDULE_REQ = _BASE_URL + "api/switchhomeschedule" + + + +class NoSchedule(Exception): + pass class HomeData: @@ -101,6 +107,32 @@ def getSelectedschedule(self, home=None): if "selected" in self.schedule[key].keys(): return self.schedule[key] + def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): + if home is None: + home = self.default_home + home_id = self.gethomeId(home=home) + schedules = { + self.schedules[home][s]["name"]: self.schedules[home][s]["id"] + for s in self.schedules[home] + } + if schedule is None and schedule_id is not None: + if schedule_id not in schedules.items(): + raise NoSchedule("%s is not a valid schedule" % schedule_id) + elif schedule_id is None and schedule is not None: + if schedule not in schedules.keys(): + raise NoSchedule("%s is not a valid schedule" % schedule) + schedule_id = schedules[schedule] + else: + raise NoSchedule("No schedule specified") + postParams = { + "access_token": self.getAuthToken, + "home_id": home_id, + "schedule_id": schedule_id, + } + resp = postRequest(_SWITCHHOMESCHEDULE_REQ, postParams) + LOG.debug("Response: %s", resp) + + class HomeStatus(HomeData): """ From f97ed27dda99348938259503d427031d0cb39420 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Wed, 8 May 2019 14:24:40 +0200 Subject: [PATCH 059/333] black format to code Signed-off-by: Hugo D. (jabesq) --- pyatmo.py | 19 ++++++++++++------- smart_home/Thermostat.py | 6 ++---- smart_home/WeatherStation.py | 4 +--- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/pyatmo.py b/pyatmo.py index 317e8cc83..3c40b4324 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -141,8 +141,13 @@ def __init__(self, authData): try: import os - if (os.environ["CLIENT_ID"] and os.environ["CLIENT_SECRET"] and - os.environ["USERNAME"] and os.environ["PASSWORD"]): + + if ( + os.environ["CLIENT_ID"] + and os.environ["CLIENT_SECRET"] + and os.environ["USERNAME"] + and os.environ["PASSWORD"] + ): _CLIENT_ID = os.environ["CLIENT_ID"] _CLIENT_SECRET = os.environ["CLIENT_SECRET"] _USERNAME = os.environ["USERNAME"] @@ -159,11 +164,11 @@ def __init__(self, authData): exit(1) authorization = ClientAuth( - clientId = _CLIENT_ID, - clientSecret = _CLIENT_SECRET, - username = _USERNAME, - password = _PASSWORD, - scope="read_station read_camera access_camera read_thermostat write_thermostat read_presence access_presence" + clientId=_CLIENT_ID, + clientSecret=_CLIENT_SECRET, + username=_USERNAME, + password=_PASSWORD, + scope="read_station read_camera access_camera read_thermostat write_thermostat read_presence access_presence", ) # Test authentication method try: diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index e3263171c..5f2fbbcd4 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -19,7 +19,6 @@ _SWITCHHOMESCHEDULE_REQ = _BASE_URL + "api/switchhomeschedule" - class NoSchedule(Exception): pass @@ -94,7 +93,7 @@ def gethomeId(self, home=None): home = self.default_home for key, value in self.homes.items(): if value["name"] == home: - LOG.debug(self.homes[key]['id']) + LOG.debug(self.homes[key]["id"]) LOG.debug(self.default_home) if "therm_schedules" in self.homes[key]: return self.homes[key]["id"] @@ -133,7 +132,6 @@ def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): LOG.debug("Response: %s", resp) - class HomeStatus(HomeData): """ """ @@ -145,7 +143,7 @@ def __init__(self, authData, home_id=None, home=None): # print(home_data.modules) if home_id: self.home_id = home_id - LOG.debug('home_id', self.home_id) + LOG.debug("home_id", self.home_id) elif home: self.home_id = self.home_data.gethomeId(home=home) else: diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 7c3c2c98a..1b745fc92 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -98,9 +98,7 @@ def monitoredConditions(self, module): ["windangle", "windstrength", "gustangle", "guststrength"] ) elif cond == "Rain": - conditions.extend( - ["Rain", "sum_rain_24", "sum_rain_1"] - ) + conditions.extend(["Rain", "sum_rain_24", "sum_rain_1"]) else: conditions.append(cond.lower()) if mod["type"] == "NAMain" or mod["type"] == "NHC": From 6b33c0303727b8f3c420e583106b66963b361eaa Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Wed, 8 May 2019 14:28:06 +0200 Subject: [PATCH 060/333] v1.11 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e2cfbce91..bde0c4c76 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="1.10", # Should be updated with new versions + version="1.11", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From 26c009eb4d63265334250c2b2cebe9b242e8dd53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 23 May 2019 22:42:08 +0200 Subject: [PATCH 061/333] Except timeout (#51) --- smart_home/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/smart_home/__init__.py b/smart_home/__init__.py index 1eb412359..e921c2cb2 100644 --- a/smart_home/__init__.py +++ b/smart_home/__init__.py @@ -1,9 +1,9 @@ +import json +import logging +import socket import time import traceback from sys import version_info -import json -import logging - LOG = logging.getLogger(__name__) @@ -39,7 +39,7 @@ def postRequest(url, params=None, timeout=10): if params else urllib.request.urlopen(req, timeout=timeout) ) - except urllib.error.URLError: + except (urllib.error.URLError, socket.timeout): LOG.error(traceback.format_exc()) return None else: From cdeaab66b2e47e400f0112977fca87af16592eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Fri, 24 May 2019 08:45:15 +0200 Subject: [PATCH 062/333] Check response for WeatherStation (#50) --- smart_home/WeatherStation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 1b745fc92..40fc0f8c9 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -21,7 +21,9 @@ def __init__(self, authData, urlReq=None): self.getAuthToken = authData.accessToken postParams = {"access_token": self.getAuthToken} resp = postRequest(self.urlReq, postParams) - self.rawData = resp["body"]["devices"] + if resp is None: + raise NoDevice("No weather station available") + self.rawData = resp["body"].get("devices") if not self.rawData: raise NoDevice("No weather station available") self.stations = {d["_id"]: d for d in self.rawData} From bffaa452ace7cb5f8332a7e73257f90b082d1185 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Fri, 24 May 2019 22:00:31 +0200 Subject: [PATCH 063/333] v1.12 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bde0c4c76..c775777e7 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="1.11", # Should be updated with new versions + version="1.12", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From c1352c5725336308ff87dd2ef18e07bd15f58316 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 25 May 2019 13:59:02 +0200 Subject: [PATCH 064/333] Drop python 2 support (#53) * Drop python 2 support * Remove version_info import --- smart_home/__init__.py | 54 ++++++++++++------------------------------ 1 file changed, 15 insertions(+), 39 deletions(-) diff --git a/smart_home/__init__.py b/smart_home/__init__.py index e921c2cb2..a465d36a3 100644 --- a/smart_home/__init__.py +++ b/smart_home/__init__.py @@ -3,20 +3,13 @@ import socket import time import traceback -from sys import version_info +import urllib.parse, urllib.request LOG = logging.getLogger(__name__) # Common definitions _BASE_URL = "https://api.netatmo.com/" -# HTTP libraries depends upon Python 2 or 3 -if version_info.major == 3: - import urllib.parse, urllib.request -else: - from urllib import urlencode - import urllib2 - class NoDevice(Exception): pass @@ -26,45 +19,28 @@ class NoDevice(Exception): def postRequest(url, params=None, timeout=10): - if version_info.major == 3: - req = urllib.request.Request(url) - if params: - req.add_header( - "Content-Type", "application/x-www-form-urlencoded;charset=utf-8" - ) - params = urllib.parse.urlencode(params).encode("utf-8") - try: - resp = ( - urllib.request.urlopen(req, params, timeout=timeout) - if params - else urllib.request.urlopen(req, timeout=timeout) - ) - except (urllib.error.URLError, socket.timeout): - LOG.error(traceback.format_exc()) - return None - else: - if params: - params = urlencode(params) - headers = { - "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" - } - req = ( - urllib2.Request(url=url, data=params, headers=headers) + req = urllib.request.Request(url) + if params: + req.add_header( + "Content-Type", "application/x-www-form-urlencoded;charset=utf-8" + ) + params = urllib.parse.urlencode(params).encode("utf-8") + try: + resp = ( + urllib.request.urlopen(req, params, timeout=timeout) if params - else urllib2.Request(url) + else urllib.request.urlopen(req, timeout=timeout) ) - try: - resp = urllib2.urlopen(req, timeout=timeout) - except urllib2.URLError: - return None + except (urllib.error.URLError, socket.timeout): + LOG.debug("params: %s", params) + LOG.error(traceback.format_exc()) + return None data = b"" for buff in iter(lambda: resp.read(65535), b""): data += buff # Return values in bytes if not json data to handle properly camera images returnedContentType = ( resp.getheader("Content-Type") - if version_info.major == 3 - else resp.info()["Content-Type"] ) return ( json.loads(data.decode("utf-8")) From 41c1e4bd46baa033e55dc4e420ab920da9659359 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 25 May 2019 14:01:21 +0200 Subject: [PATCH 065/333] Add more debug messages (#52) * Try to catch KeyError in Camera setup * Try to catch KeyErrors in Thermostat setup * Directly query the homes dict * Add debug message to postRequest function * Use .get() * Change try to if --- smart_home/Camera.py | 29 +++++++++++++++++------------ smart_home/Thermostat.py | 12 +++++++++--- smart_home/WeatherStation.py | 2 +- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index 8c208bda9..2261da790 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -29,10 +29,12 @@ def __init__(self, authData, size=15): self.getAuthToken = authData.accessToken postParams = {"access_token": self.getAuthToken, "size": size} resp = postRequest(_GETHOMEDATA_REQ, postParams) - if "body" not in resp: - raise URLError("No data returned by Netatmo server") - self.rawData = resp["body"] - self.homes = {d["id"]: d for d in self.rawData["homes"]} + if resp is None: + raise URLError("No camera data returned by Netatmo server") + self.rawData = resp["body"].get("homes") + if not self.rawData: + raise NoDevice("No camera data available") + self.homes = {d["id"]: d for d in self.rawData} if not self.homes: raise NoDevice("No camera available") self.persons = dict() @@ -45,17 +47,20 @@ def __init__(self, authData, size=15): self.types = dict() self.default_home = None self.default_camera = None - for i in range(len(self.rawData["homes"])): - nameHome = self.rawData["homes"][i]["name"] + for i in range(len(self.rawData)): + nameHome = self.rawData[i].get("name") + if not nameHome: + raise NoDevice("No key [\"name\"] in %s" % + self.rawData[i].keys()) if nameHome not in self.cameras: self.cameras[nameHome] = dict() if nameHome not in self.types: self.types[nameHome] = dict() - for p in self.rawData["homes"][i]["persons"]: + for p in self.rawData[i]["persons"]: self.persons[p["id"]] = p - if "events" in self.rawData["homes"][i]: - self.default_home = self.rawData["homes"][i]["name"] - for e in self.rawData["homes"][i]["events"]: + if "events" in self.rawData[i]: + self.default_home = self.rawData[i]["name"] + for e in self.rawData[i]["events"]: if e["type"] == "outdoor": if e["camera_id"] not in self.outdoor_events: self.outdoor_events[e["camera_id"]] = dict() @@ -64,13 +69,13 @@ def __init__(self, authData, size=15): if e["camera_id"] not in self.events: self.events[e["camera_id"]] = dict() self.events[e["camera_id"]][e["time"]] = e - for c in self.rawData["homes"][i]["cameras"]: + for c in self.rawData[i]["cameras"]: self.cameras[nameHome][c["id"]] = c if c["type"] == "NACamera" and "modules" in c: for m in c["modules"]: self.modules[m["id"]] = m self.modules[m["id"]]["cam_id"] = c["id"] - for t in self.rawData["homes"][i]["cameras"]: + for t in self.rawData[i]["cameras"]: self.types[nameHome][t["type"]] = t for camera in self.events: self.lastEvent[camera] = self.events[camera][ diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 5f2fbbcd4..66a00237c 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -35,8 +35,11 @@ def __init__(self, authData): self.getAuthToken = authData.accessToken postParams = {"access_token": self.getAuthToken} resp = postRequest(_GETHOMESDATA_REQ, postParams) - - self.rawData = resp["body"]["homes"] + if resp is None: + raise NoDevice("No thermostat data returned by Netatmo server") + self.rawData = resp["body"].get("homes") + if not self.rawData: + raise NoDevice("No thermostat data available") self.homes = {d["id"]: d for d in self.rawData} if not self.homes: raise NoDevice("No thermostat available") @@ -46,7 +49,10 @@ def __init__(self, authData): self.zones = dict() self.setpoint_duration = dict() for i in range(len(self.rawData)): - nameHome = self.rawData[i]["name"] + nameHome = self.rawData[i].get("name") + if not nameHome: + raise NoDevice("No key [\"name\"] in %s", + self.rawData[i].keys()) if "modules" in self.rawData[i]: if nameHome not in self.modules: self.modules[nameHome] = dict() diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 40fc0f8c9..9f6c0c969 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -22,7 +22,7 @@ def __init__(self, authData, urlReq=None): postParams = {"access_token": self.getAuthToken} resp = postRequest(self.urlReq, postParams) if resp is None: - raise NoDevice("No weather station available") + raise NoDevice("No weather station data returned by Netatmo server") self.rawData = resp["body"].get("devices") if not self.rawData: raise NoDevice("No weather station available") From 7ee1373c608c90f5d7053871d6c66129f978b1d0 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sat, 25 May 2019 14:03:15 +0200 Subject: [PATCH 066/333] Blacken code Signed-off-by: Hugo D. (jabesq) --- smart_home/Camera.py | 3 +-- smart_home/Thermostat.py | 3 +-- smart_home/__init__.py | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index 2261da790..b8c7da4c8 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -50,8 +50,7 @@ def __init__(self, authData, size=15): for i in range(len(self.rawData)): nameHome = self.rawData[i].get("name") if not nameHome: - raise NoDevice("No key [\"name\"] in %s" % - self.rawData[i].keys()) + raise NoDevice('No key ["name"] in %s' % self.rawData[i].keys()) if nameHome not in self.cameras: self.cameras[nameHome] = dict() if nameHome not in self.types: diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 66a00237c..63a6c55cc 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -51,8 +51,7 @@ def __init__(self, authData): for i in range(len(self.rawData)): nameHome = self.rawData[i].get("name") if not nameHome: - raise NoDevice("No key [\"name\"] in %s", - self.rawData[i].keys()) + raise NoDevice('No key ["name"] in %s', self.rawData[i].keys()) if "modules" in self.rawData[i]: if nameHome not in self.modules: self.modules[nameHome] = dict() diff --git a/smart_home/__init__.py b/smart_home/__init__.py index a465d36a3..640d2abb5 100644 --- a/smart_home/__init__.py +++ b/smart_home/__init__.py @@ -39,9 +39,7 @@ def postRequest(url, params=None, timeout=10): for buff in iter(lambda: resp.read(65535), b""): data += buff # Return values in bytes if not json data to handle properly camera images - returnedContentType = ( - resp.getheader("Content-Type") - ) + returnedContentType = resp.getheader("Content-Type") return ( json.loads(data.decode("utf-8")) if "application/json" in returnedContentType From 2c5f7e822cb9f81ff272a3c84945437268490de2 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sat, 25 May 2019 14:22:00 +0200 Subject: [PATCH 067/333] Sort imports with isort Signed-off-by: Hugo D. (jabesq) --- .isort.cfg | 6 ++++++ pyatmo.py | 10 +++++----- samples/graphLast3Days | 6 +++--- samples/printAllLastData | 1 + samples/simpleLastData | 1 - samples/smsAlarm | 7 +++++-- setup.py | 2 +- smart_home/Camera.py | 5 ++--- smart_home/HomeCoach.py | 3 +-- smart_home/PublicData.py | 2 +- smart_home/Thermostat.py | 5 ++--- smart_home/WeatherStation.py | 5 +++-- 12 files changed, 30 insertions(+), 23 deletions(-) create mode 100644 .isort.cfg diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 000000000..0ac773da8 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,6 @@ +[settings] +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True +line_length=88 \ No newline at end of file diff --git a/pyatmo.py b/pyatmo.py index 3c40b4324..bc06d61b6 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -10,15 +10,15 @@ PythonAPI Netatmo REST data access coding=utf-8 """ -import time import logging +import time -from smart_home.WeatherStation import WeatherStationData, DeviceList +from smart_home import _BASE_URL, NoDevice, postRequest from smart_home.Camera import CameraData -from smart_home.Thermostat import ThermostatData, HomeData, HomeStatus -from smart_home.PublicData import PublicData from smart_home.HomeCoach import HomeCoachData -from smart_home import _BASE_URL, postRequest, NoDevice +from smart_home.PublicData import PublicData +from smart_home.Thermostat import HomeData, HomeStatus, ThermostatData +from smart_home.WeatherStation import DeviceList, WeatherStationData LOG = logging.getLogger(__name__) diff --git a/samples/graphLast3Days b/samples/graphLast3Days index fe3e97e22..a37579cbe 100755 --- a/samples/graphLast3Days +++ b/samples/graphLast3Days @@ -6,12 +6,12 @@ # This is an example of graphing Temperature and Humidity from a module on the last 3 days # The Matplotlib library is used and should be installed before running this sample program -import datetime, time +import datetime +import time import lnetatmo - -from matplotlib import pyplot as plt from matplotlib import dates +from matplotlib import pyplot as plt from matplotlib.ticker import FormatStrFormatter # Access to the sensors diff --git a/samples/printAllLastData b/samples/printAllLastData index 08165423e..c13450d20 100755 --- a/samples/printAllLastData +++ b/samples/printAllLastData @@ -10,6 +10,7 @@ # significant) import time + import lnetatmo authorization = lnetatmo.ClientAuth() diff --git a/samples/simpleLastData b/samples/simpleLastData index 0d3b67847..3a333e0b3 100755 --- a/samples/simpleLastData +++ b/samples/simpleLastData @@ -16,4 +16,3 @@ print ("Current temperature (inside/outside): %s / %s °C" % ( devList.lastData()['internal']['Temperature'], devList.lastData()['external']['Temperature']) ) - diff --git a/samples/smsAlarm b/samples/smsAlarm index 82c7b74f9..cf2cf6943 100755 --- a/samples/smsAlarm +++ b/samples/smsAlarm @@ -8,8 +8,11 @@ # peoples (here phone1 and phone2 are two mobile phone numbers) # Note : lsms is my personnal library to send SMS using a GSM modem, you have to use your method/library -import sys, os -import lnetatmo,lsms +import os +import sys + +import lnetatmo +import lsms MARKER_FILE = "//TempAlarm" # This flag file will be used to avoid sending multiple SMS on the same event # Remember that the user who run the cron job must have the rights to create the file diff --git a/setup.py b/setup.py index c775777e7..597d2587d 100755 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ # python setup.py --dry-run --verbose install from distutils.core import setup -import setuptools +import setuptools setup( name="pyatmo", diff --git a/smart_home/Camera.py b/smart_home/Camera.py index b8c7da4c8..d32da5731 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -2,12 +2,11 @@ coding=utf-8 """ import imghdr -import time import logging - +import time from urllib.error import URLError -from . import NoDevice, postRequest, _BASE_URL +from . import _BASE_URL, NoDevice, postRequest LOG = logging.getLogger(__name__) diff --git a/smart_home/HomeCoach.py b/smart_home/HomeCoach.py index f0d290cd8..29f46fd40 100644 --- a/smart_home/HomeCoach.py +++ b/smart_home/HomeCoach.py @@ -1,9 +1,8 @@ """ coding=utf-8 """ -from .WeatherStation import WeatherStationData - from . import _BASE_URL +from .WeatherStation import WeatherStationData _GETHOMECOACHDATA_REQ = _BASE_URL + "api/gethomecoachsdata" diff --git a/smart_home/PublicData.py b/smart_home/PublicData.py index 9c4bd6936..3455108a1 100644 --- a/smart_home/PublicData.py +++ b/smart_home/PublicData.py @@ -3,7 +3,7 @@ """ import time -from . import postRequest, _BASE_URL +from . import _BASE_URL, postRequest _GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata" _LON_NE = 6.221652 diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 63a6c55cc..b0614da7e 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -1,11 +1,10 @@ """ coding=utf-8 """ -import time import logging +import time -from . import NoDevice, postRequest, _BASE_URL - +from . import _BASE_URL, NoDevice, postRequest LOG = logging.getLogger(__name__) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 9f6c0c969..8b02c7a28 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -1,9 +1,10 @@ """ coding=utf-8 """ -import warnings, time +import time +import warnings -from . import NoDevice, postRequest, todayStamps, _BASE_URL +from . import _BASE_URL, NoDevice, postRequest, todayStamps _GETMEASURE_REQ = _BASE_URL + "api/getmeasure" _GETSTATIONDATA_REQ = _BASE_URL + "api/getstationsdata" From 0e4b0e5438ee42e5ebfc83b73f764c1e8f6f9bad Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 28 May 2019 14:30:58 +0200 Subject: [PATCH 068/333] Fix schedule switcher by id (#56) Fix schedule switcher by id --- smart_home/Thermostat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index b0614da7e..80a479be7 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -119,8 +119,8 @@ def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): for s in self.schedules[home] } if schedule is None and schedule_id is not None: - if schedule_id not in schedules.items(): - raise NoSchedule("%s is not a valid schedule" % schedule_id) + if schedule_id not in schedules.values(): + raise NoSchedule("%s is not a valid schedule id" % schedule_id) elif schedule_id is None and schedule is not None: if schedule not in schedules.keys(): raise NoSchedule("%s is not a valid schedule" % schedule) From ad5b470fcebbd0f523a12e090f0d806123ad3e83 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 28 May 2019 16:41:32 +0200 Subject: [PATCH 069/333] Convert iterator to list (#58) --- smart_home/Thermostat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 80a479be7..d0318f3bd 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -119,10 +119,10 @@ def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): for s in self.schedules[home] } if schedule is None and schedule_id is not None: - if schedule_id not in schedules.values(): + if schedule_id not in list(schedules.values()): raise NoSchedule("%s is not a valid schedule id" % schedule_id) elif schedule_id is None and schedule is not None: - if schedule not in schedules.keys(): + if schedule not in list(schedules.keys()): raise NoSchedule("%s is not a valid schedule" % schedule) schedule_id = schedules[schedule] else: From 661d9758ca591535b7ea23a0650a5220c65ba3e7 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 29 May 2019 20:55:20 +0200 Subject: [PATCH 070/333] Clean up Python 2 references and unused code (#55) --- README.md | 4 +- pyatmo.py | 61 +++------------ samples/graphLast3Days | 72 ------------------ samples/printAllLastData | 67 ----------------- samples/simpleLastData | 18 ----- samples/smsAlarm | 51 ------------- setup.py | 2 +- usage.md | 157 ++++++++++++++------------------------- 8 files changed, 67 insertions(+), 365 deletions(-) delete mode 100755 samples/graphLast3Days delete mode 100755 samples/printAllLastData delete mode 100755 samples/simpleLastData delete mode 100755 samples/smsAlarm diff --git a/README.md b/README.md index 5f4d6dfdb..e79e1e9e5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ netatmo-api-python ================== [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) -Simple API to access Netatmo devices and data like weather station or camera data from python. +Simple API to access Netatmo devices and data like weather station or camera data from Python 3. For more detailed information see http://dev.netatmo.com This project has no relation with the Netatmo company. @@ -14,7 +14,7 @@ To install pyatmo simply run: pip install pyatmo Depending on your permissions you might be required to use sudo. -Once installed you can simple add `pyatmo` to your python scripts by including: +Once installed you can simple add `pyatmo` to your Python 3 scripts by including: import pyatmo diff --git a/pyatmo.py b/pyatmo.py index bc06d61b6..2b9abdf61 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -5,8 +5,7 @@ """ This API provides access to the Netatmo weather station or/and the Netatmo cameras or/and the Netatmo smart thermostat -This package can be used with Python2 or Python3 applications and do not -require anything else than standard libraries +This package can be used with Python3 applications PythonAPI Netatmo REST data access coding=utf-8 """ @@ -22,21 +21,6 @@ LOG = logging.getLogger(__name__) -######################## USER SPECIFIC INFORMATION ###################### - -# To be able to have a program accessing your netatmo data, you have to register your program as -# a Netatmo app in your Netatmo account. All you have to do is to give it a name (whatever) and you will be -# returned a client_id and secret that your app has to supply to access netatmo servers. - -_CLIENT_ID = ( - "" -) # Your client ID from Netatmo app registration at http://dev.netatmo.com/dev/listapps -_CLIENT_SECRET = "" # Your client app secret ' ' -_USERNAME = "" # Your netatmo account username -_PASSWORD = "" # Your netatmo account password - -######################################################################### - # Common definitions _AUTH_REQ = _BASE_URL + "oauth2/token" @@ -64,12 +48,7 @@ class ClientAuth: """ def __init__( - self, - clientId=_CLIENT_ID, - clientSecret=_CLIENT_SECRET, - username=_USERNAME, - password=_PASSWORD, - scope="read_station", + self, clientId, clientSecret, username, password, scope="read_station" ): postParams = { "grant_type": "password", @@ -118,21 +97,6 @@ def accessToken(self): return self._accessToken -class User: - """ - This class returns basic information about the user - Args: - authData (ClientAuth): Authentication information with a working access Token - """ - - def __init__(self, authData): - postParams = {"access_token": authData.accessToken} - resp = postRequest(_GETSTATIONDATA_REQ, postParams) - self.rawData = resp["body"] - self.devList = self.rawData["devices"] - self.ownerMail = self.rawData["user"]["mail"] - - # auto-test when executed directly if __name__ == "__main__": @@ -148,26 +112,21 @@ def __init__(self, authData): and os.environ["USERNAME"] and os.environ["PASSWORD"] ): - _CLIENT_ID = os.environ["CLIENT_ID"] - _CLIENT_SECRET = os.environ["CLIENT_SECRET"] - _USERNAME = os.environ["USERNAME"] - _PASSWORD = os.environ["PASSWORD"] + CLIENT_ID = os.environ["CLIENT_ID"] + CLIENT_SECRET = os.environ["CLIENT_SECRET"] + USERNAME = os.environ["USERNAME"] + PASSWORD = os.environ["PASSWORD"] except KeyError: stderr.write( "No credentials passed to pyatmo.py (client_id, client_secret, username, password)\n" ) - - if not _CLIENT_ID or not _CLIENT_SECRET or not _USERNAME or not _PASSWORD: - stderr.write( - "Library source missing identification arguments to check pyatmo.py (user/password/etc...)\n" - ) exit(1) authorization = ClientAuth( - clientId=_CLIENT_ID, - clientSecret=_CLIENT_SECRET, - username=_USERNAME, - password=_PASSWORD, + clientId=CLIENT_ID, + clientSecret=CLIENT_SECRET, + username=USERNAME, + password=PASSWORD, scope="read_station read_camera access_camera read_thermostat write_thermostat read_presence access_presence", ) # Test authentication method diff --git a/samples/graphLast3Days b/samples/graphLast3Days deleted file mode 100755 index a37579cbe..000000000 --- a/samples/graphLast3Days +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/python -# coding=utf-8 - -# 2013-01 : philippelt@users.sourceforge.net - -# This is an example of graphing Temperature and Humidity from a module on the last 3 days -# The Matplotlib library is used and should be installed before running this sample program - -import datetime -import time - -import lnetatmo -from matplotlib import dates -from matplotlib import pyplot as plt -from matplotlib.ticker import FormatStrFormatter - -# Access to the sensors -auth = lnetatmo.ClientAuth() -dev = lnetatmo.DeviceList(auth) - -# Time of information collection : 3*24hours windows to now -now = time.time() -start = now - 3 * 24 * 3600 - -# Get Temperature and Humidity with GETMEASURE web service (1 sample every 30min) -resp = dev.getMeasure( device_id='xxxx', # Replace with your values - module_id='xxxx', # " " " " - scale="30min", - mtype="Temperature,Humidity", - date_begin=start, - date_end=now) - -# Extract the timestamp, temperature and humidity from the more complex response structure -result = [(int(k),v[0],v[1]) for k,v in resp['body'].items()] -# Sort samples by timestamps (Warning, they are NOT sorted by default) -result.sort() -# Split in 3 lists for use with Matplotlib (timestamp on x, temperature and humidity on two y axis) -xval, ytemp, yhum = zip(*result) - -# Convert the x axis values from Netatmo timestamp to matplotlib timestamp... -xval = [dates.date2num(datetime.datetime.fromtimestamp(x)) for x in xval] - -# Build the two curves graph (check Matplotlib documentation for details) -fig = plt.figure() -plt.xticks(rotation='vertical') - -graph1 = fig.add_subplot(111) - -graph1.plot(xval, ytemp, color='r', linewidth=3) -graph1.set_ylabel(u'TempÊrature', color='r') -graph1.set_ylim(0, 25) -graph1.yaxis.grid(color='gray', linestyle='dashed') -for t in graph1.get_yticklabels() : t.set_color('r') -graph1.yaxis.set_major_formatter(FormatStrFormatter(u'%2.0f °C')) - -graph2 = graph1.twinx() - -graph2.plot(xval, yhum, color='b', linewidth=3) -graph2.set_ylabel(u'HumiditÊ',color='b') -graph2.set_ylim(50,100) -for t in graph2.get_yticklabels(): t.set_color('b') -graph2.yaxis.set_major_formatter(FormatStrFormatter(u'%2i %%')) - -graph1.xaxis.set_major_locator(dates.HourLocator(interval=6)) -graph1.xaxis.set_minor_locator(dates.HourLocator()) -graph1.xaxis.set_major_formatter(dates.DateFormatter("%d-%Hh")) -graph1.xaxis.grid(color='gray') -graph1.set_xlabel(u'Jour et heure de la journÊe') - -# X display the resulting graph (you could generate a PDF/PNG/... in place of display). -# The display provides a minimal interface that notably allows you to save your graph -plt.show() diff --git a/samples/printAllLastData b/samples/printAllLastData deleted file mode 100755 index c13450d20..000000000 --- a/samples/printAllLastData +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/python3 -# encoding=utf-8 - -# 2014-01 : philippelt@users.sourceforge.net - -# Just connect to a Netatmo account, and print all last informations available for -# station and modules of the user account -# (except if module data is more than one hour old that usually means module lost -# wether out of radio range or battery exhausted thus information is no longer -# significant) - -import time - -import lnetatmo - -authorization = lnetatmo.ClientAuth() -devList = lnetatmo.WeatherStationData(authorization) - -# For each available module in the returned data that should not be older than one hour (3600 s) from now -for module, moduleData in devList.lastData(exclude=3600).items() : - - # Name of the module (or station embedded module), the name you defined in the web netatmo account station management - print(module) - - # List key/values pair of sensor information (eg Humidity, Temperature, etc...) - for sensor, value in moduleData.items() : - # To ease reading, print measurement event in readable text (hh:mm:ss) - if sensor == "When" : value = time.strftime("%H:%M:%S",time.localtime(value)) - print("%30s : %s" % (sensor, value)) - - -# OUTPUT SAMPLE : -# -# $ printAllLastData -# -#Office -# AbsolutePressure : 988.7 -# CO2 : 726 -# date_max_temp : 1400760301 -# date_min_temp : 1400736146 -# Humidity : 60 -# max_temp : 19.6 -# min_temp : 17.9 -# Noise : 46 -# Particle : 12768 -# Pressure : 988.7 -# Temperature : 19.6 -# When : 14:10:01 -#Outdoor -# battery_vp : 5200 -# CO2 : 555 -# date_max_temp : 1400759951 -# date_min_temp : 1400732524 -# Humidity : 75 -# max_temp : 17.9 -# min_temp : 10.3 -# rf_status : 57 -# Temperature : 17.9 -# When : 14:09:25 -#Greenhouse -# date_min_temp : 1400732204 -# Humidity : 89 -# max_temp : 19.9 -# min_temp : 9.1 -# rf_status : 83 -# Temperature : 19.9 -# When : 14:09:12 diff --git a/samples/simpleLastData b/samples/simpleLastData deleted file mode 100755 index 3a333e0b3..000000000 --- a/samples/simpleLastData +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/python3 -# encoding=utf-8 - -# 2013-01 : philippelt@users.sourceforge.net - -# Just connect to a Netatmo account, and print internal and external temperature of the default (or single) station -# In this case, sensors of the station and the external module have been named 'internal' and 'external' in the -# Account station settings. - -import lnetatmo - -authorization = lnetatmo.ClientAuth() -devList = lnetatmo.WeatherStationData(authorization) - -print ("Current temperature (inside/outside): %s / %s °C" % - ( devList.lastData()['internal']['Temperature'], - devList.lastData()['external']['Temperature']) - ) diff --git a/samples/smsAlarm b/samples/smsAlarm deleted file mode 100755 index cf2cf6943..000000000 --- a/samples/smsAlarm +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/python3 -# encoding=utf-8 - -# 2013-01 : philippelt@users.sourceforge.net - -# Simple example run in a cron job (every 30' for example) to send an alarm SMS if some condition is reached -# and an other SMS when condition returned to normal. In both case, a single SMS is emitted to multiple -# peoples (here phone1 and phone2 are two mobile phone numbers) -# Note : lsms is my personnal library to send SMS using a GSM modem, you have to use your method/library - -import os -import sys - -import lnetatmo -import lsms - -MARKER_FILE = "//TempAlarm" # This flag file will be used to avoid sending multiple SMS on the same event - # Remember that the user who run the cron job must have the rights to create the file - -# Access the station -authorization = lnetatmo.ClientAuth() -devList = lnetatmo.WeatherStationData(authorization) - -message = [] - -# Condition 1 : the external temperature is below our limit -curT = devList.lastData()['external']['Temperature'] -if curT < 5 : message.append("Temperature going below 5°C") - -# Condition 2 : The external temperature data is older that 1 hour -someLost = devList.checkNotUpdated() -if someLost and 'external' in someLost : message.append("Sensor is no longer active") - -# Condition 3 : The outdoor module battery is dying -volts = devList.lastData()['external']['battery_vp'] # I suspect that this is the total Voltage in mV -if volts < 5000 : message.append("External module battery needs replacement") # I will adjust the threshold over time - -# If one condition is present, at least, send an alarm by SMS -if message : - if not os.path.exists(MARKER_FILE) : - message = "WEATHER ALERT\n" + "\n".join(message) - for p in ('', '') : - lsms.sendSMS(p, message, flash=True) - open(MARKER_FILE,"w").close() # Just to create the empty marker file and avoid to resend the same alert -else : - if os.path.exists(MARKER_FILE) : - os.remove(MARKER_FILE) - for p in ('', '') : - lsms.sendSMS(p, "END of WEATHER alert, current temperature is %s°C" % curT) - -sys.exit(0) diff --git a/setup.py b/setup.py index 597d2587d..925f3fbcf 100755 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ data_files=[], url="https://github.com/jabesq/netatmo-api-python", license="MIT", - description="Simple API to access Netatmo weather station data from any python script. " + description="Simple API to access Netatmo weather station data from any Python 3 script. " "Design for Home-Assitant (but not only)", long_description=open("README.md").read(), ) diff --git a/usage.md b/usage.md index 4ee337c8c..fc286bd24 100644 --- a/usage.md +++ b/usage.md @@ -12,9 +12,7 @@ Python Netatmo API programmers guide >2016-12-09 Update documentation for all Netatmo cameras -No additional library other than standard Python library is required. - -Both Python V2.7x and V3.x.x are supported without change. +No additional library other than standard Python 3 library is required. More information about the Netatmo REST API can be obtained from http://dev.netatmo.com/doc/ @@ -40,28 +38,19 @@ In the netatmo philosophy, both the application itself and the user have to be r -Copy the lnetatmo.py file in your work directory (or your platform choice of user libraries or virtualenv or ...). - -To ease future uses, I suggest that you hardcode in the library your application and user credentials. This is not mandatory as this parameters can be explicitly passed at authentication phase but will save you parameters each time you write a new tool. - -If you want to do it, just edit the source file and hard code required values for : - - -```python -_CLIENT_ID = "" -_CLIENT_SECRET = "" -_USERNAME = "" -_PASSWORD = "" -``` - +Install `pyatmo` as described in the `README.md`. -If you provide all the values, you can test that everything is working properly by simply running the package as a standalone program. +If you provide your credentials, you can test if everything is working properly by simply running the package as a standalone program. 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. ```bash -$ python3 lnetatmo.py # or python2 as well -lnetatmo.py : OK +$ export CLIENT_ID="" +$ export CLIENT_SECRET="" +$ export USERNAME="" +$ export PASSWORD="" +$ python3 pyatmo.py +pyatmo.py : OK $ echo $? 0 ``` @@ -82,25 +71,34 @@ Most of the time, the sequence of operations will be : Example : ```python -#!/usr/bin/python3 -# encoding=utf-8 - -import lnetatmo +import pyatmo # 1 : Authenticate -authorization = lnetatmo.ClientAuth() +CLIENT_ID = '123456789abcd1234' +CLIENT_SECRET = '123456789abcd1234' +USERNAME = 'your@account.com' +PASSWORD = 'abcdef-123456-ghijkl' +authorization = pyatmo.ClientAuth( + clientId=CLIENT_ID, + clientSecret=CLIENT_SECRET, + username=USERNAME, + password=PASSWORD, +) # 2 : Get devices list -weatherData = lnetatmo.WeatherStationData(authorization) +weatherData = pyatmo.WeatherStationData(authorization) # 3 : Access most fresh data directly -print ("Current temperature (inside/outside): %s / %s °C" % - ( weatherData.lastData()['indoor']['Temperature'], - weatherData.lastData()['outdoor']['Temperature']) +print( + "Current temperature (inside/outside): %s / %s °C" + % ( + weatherData.lastData()["indoor"]["Temperature"], + weatherData.lastData()["outdoor"]["Temperature"], + ) ) ``` -In this example, no init parameters are supplied to ClientAuth, the library is supposed to have been customized with the required values (see §2). The user must have named the sensors indoor and outdoor through the Web interface (or any other name as long as the program is requesting the same name). +The user must have named the sensors indoor and outdoor through the Web interface (or any other name as long as the program is requesting the same name). The Netatmo design is based on stations (usually the in-house module) and modules (radio sensors reporting to a station, usually an outdoor sensor). @@ -129,11 +127,6 @@ The results are Python data structures, mostly dictionaries as they mirror easil ```python -_CLIENT_ID, _CLIENT_SECRET = Application ID and secret provided by Netatmo -application registration in your user account - -_USERNAME, _PASSWORD : Username and password of your netatmo account - _BASE_URL and _*_REQ : Various URL to access Netatmo web services. They are documented in http://dev.netatmo.com/doc/ They should not be changed unless Netatmo API changes. @@ -148,16 +141,17 @@ Netatmo API changes. Constructor ```python - authorization = lnetatmo.ClientAuth( clientId = _CLIENT_ID, - clientSecret = _CLIENT_SECRET, - username = _USERNAME, - password = _PASSWORD, - scope = "read_station" - ) +authorization = pyatmo.ClientAuth( + clientId=CLIENT_ID, + clientSecret=CLIENT_SECRET, + username=USERNAME, + password=PASSWORD, + scope="read_station", +) ``` -Requires : Application and User credentials to access Netatmo API. if all this parameters are put in global variables they are not required (in library source code or in the main program through lnetatmo._CLIENT_ID = â€Ļ) +Requires : Application and User credentials to access Netatmo API. Return : an authorization object that will supply the access token required by other web services. This class will handle the renewal of the access token if expiration is reached. @@ -183,43 +177,14 @@ Several value can be used at the same time, ie: 'read_station read_camera' -#### 4-3 User class #### - - - -Constructor - -```python - user = lnetatmo.User( authorization ) -``` - - -Requires : an authorization object (ClientAuth instance) - - -Return : a User object. This object provides multiple informations on the user account such as the mail address of the user, the preferred language, â€Ļ - - -Properties, all properties are read-only unless specified : - - - * **rawData** : Full dictionary of the returned JSON GETUSER Netatmo API service - * **ownerMail** : eMail address associated to the user account - * **devList** : List of Station's id accessible to the user account - - -In most cases, you will not need to use this class that is oriented toward an application that would use the other authentication method to an unknown user and then get information about him. - - - -#### 4-4 WeatherStationData class #### +#### 4-3 WeatherStationData class #### Constructor ```python - weatherData = lnetatmo.WeatherStationData( authorization ) +weatherData = pyatmo.WeatherStationData(authorization) ``` @@ -228,7 +193,7 @@ Requires : an authorization object (ClientAuth instance) 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. -Raise a lnetatmo.NoDevice exception if no weather station is available for the given account. +Raise a pyatmo.NoDevice exception if no weather station is available for the given account. Properties, all properties are read-only unless specified: @@ -279,11 +244,11 @@ Methods : # Last data access example theData = weatherData.lastData() -print('Available modules : ', theData.keys() ) -print('In-house CO2 level : ', theData['indoor']['Co2'] ) -print('Outside temperature : ', theData['outdoor']['Temperature'] ) +print('Available modules : ', theData.keys()) +print('In-house CO2 level : ', theData['indoor']['Co2']) +print('Outside temperature : ', theData['outdoor']['Temperature']) print('External module battery : ', "OK" if int(theData['outdoor']['battery_vp']) > 5000 \ - else "NEEDS TO BE REPLACED" ) + else "NEEDS TO BE REPLACED") ``` * **checkNotUpdated** (station=None, delay=3600) : * Input : optional station name (else default_station is used) @@ -322,14 +287,14 @@ for m in weatherData.checkNotUpdated(""): at all if you slip over two days as required in a shifting 24 hours window. -#### 4-5 CameraData class #### +#### 4-4 CameraData class #### Constructor ```python - cameraData = lnetatmo.CameraData( authorization ) +cameraData = pyatmo.CameraData( authorization ) ``` @@ -338,7 +303,7 @@ Requires : an authorization object (ClientAuth instance) Return : a CameraData object. This object contains most administration properties of Netatmo cameras accessible to the user and the last data pushed by the cameras to the Netatmo servers. -Raise a lnetatmo.NoDevice exception if no camera is available for the given account. +Raise a pyatmo.NoDevice exception if no camera is available for the given account. Properties, all properties are read-only unless specified: @@ -410,14 +375,16 @@ Methods : * **carDetected** (home=None, camera=None) : Return True if a car has been detected in the last outdoor events - #### 4-6 ThermostatData class #### + + + #### 4-5 ThermostatData class #### Constructor ```python - thermostatData = lnetatmo.ThermostatData( authorization ) + thermostatData = pyatmo.ThermostatData(authorization) ``` @@ -426,7 +393,7 @@ Methods : Return : a ThermostatData object. This object contains most administration properties of Netatmo thermostats accessible to the user and the last data pushed by the thermostats to the Netatmo servers. - Raise a lnetatmo.NoDevice exception if no thermostat is available for the given account. + Raise a pyatmo.NoDevice exception if no thermostat is available for the given account. Properties, all properties are read-only unless specified: @@ -464,28 +431,12 @@ Methods : * Input : device_id and module_id and setpoint_mode -#### 4-7 Utilities functions #### + + +#### 4-6 Utilities functions #### * **toTimeString** (timestamp) : Convert a Netatmo time stamp to a readable date/time format. * **toEpoch**( dateString) : Convert a date string (form YYYY-MM-DD_HH:MM:SS) to timestamp * **todayStamps**() : Return a couple of epoch time (start, end) for the current day - -#### 4-8 All-in-One function #### - - -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 : - - -**getStationMinMaxTH**(station=None, module=None) : - * 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). - * Output : A tuple of 6 values (Temperature, Humidity, minT, MaxT, minH, maxH) - -```python ->>> import lnetatmo ->>> print(lnetatmo.getStationMinMaxTH()) -[20, 33, 18.1, 20, 30, 34] ->>> ->>> print(lnetatmo.getStationMinMaxTH(module='outdoor')) -[2, 53, 1.2, 5.4, 51, 74] From 1c36f6bdfaeb8161f70010aae43e12bdd4801798 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 29 May 2019 20:56:21 +0200 Subject: [PATCH 071/333] Replace urllib.request by requests (#54) --- smart_home/__init__.py | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/smart_home/__init__.py b/smart_home/__init__.py index 640d2abb5..fa19fbcd4 100644 --- a/smart_home/__init__.py +++ b/smart_home/__init__.py @@ -3,7 +3,8 @@ import socket import time import traceback -import urllib.parse, urllib.request + +import requests LOG = logging.getLogger(__name__) @@ -19,31 +20,11 @@ class NoDevice(Exception): def postRequest(url, params=None, timeout=10): - req = urllib.request.Request(url) - if params: - req.add_header( - "Content-Type", "application/x-www-form-urlencoded;charset=utf-8" - ) - params = urllib.parse.urlencode(params).encode("utf-8") - try: - resp = ( - urllib.request.urlopen(req, params, timeout=timeout) - if params - else urllib.request.urlopen(req, timeout=timeout) - ) - except (urllib.error.URLError, socket.timeout): - LOG.debug("params: %s", params) - LOG.error(traceback.format_exc()) - return None - data = b"" - for buff in iter(lambda: resp.read(65535), b""): - data += buff - # Return values in bytes if not json data to handle properly camera images - returnedContentType = resp.getheader("Content-Type") + resp = requests.post(url, data=params, timeout=timeout) return ( - json.loads(data.decode("utf-8")) - if "application/json" in returnedContentType - else data + resp.json() + if "application/json" in resp.headers.get("content-type") + else resp.content ) From 29eb32205d959908dcaf7a7613313f43a7e1d268 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 7 Jun 2019 08:34:56 +0200 Subject: [PATCH 072/333] Return None if invalid content or server unavailable (#60) * Add homecoach scope comment * Add logging and check for body tag * Fix return if invalid content * Improve error and debug messages * Add error message if API not available * Add exception handling for public weather module --- pyatmo.py | 3 ++- smart_home/PublicData.py | 7 +++++-- smart_home/WeatherStation.py | 9 ++++++++- smart_home/__init__.py | 16 +++++++++++----- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/pyatmo.py b/pyatmo.py index 2b9abdf61..fd15c1538 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -40,9 +40,10 @@ class ClientAuth: read_station: to retrieve weather station data (Getstationsdata, Getmeasure) read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture) access_camera: to access the camera, the videos and the live stream. - read_thermostat: to retrieve thermostat data ( Getmeasure, Getthermostatsdata) + read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata) write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint) read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) + read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status Several value can be used at the same time, ie: 'read_station read_camera' """ diff --git a/smart_home/PublicData.py b/smart_home/PublicData.py index 3455108a1..25eddda71 100644 --- a/smart_home/PublicData.py +++ b/smart_home/PublicData.py @@ -3,7 +3,7 @@ """ import time -from . import _BASE_URL, postRequest +from . import _BASE_URL, NoDevice, postRequest _GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata" _LON_NE = 6.221652 @@ -51,7 +51,10 @@ def __init__( post_params["required_data"] = required_data_type resp = postRequest(_GETPUBLIC_DATA, post_params) - self.raw_data = resp["body"] + try: + self.raw_data = resp["body"] + except (KeyError, TypeError): + raise NoDevice("No public weather data returned by Netatmo server") self.status = resp["status"] self.time_exec = toTimeString(resp["time_exec"]) self.time_server = toTimeString(resp["time_server"]) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 8b02c7a28..5b57eff26 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -1,11 +1,14 @@ """ coding=utf-8 """ +import logging import time import warnings from . import _BASE_URL, NoDevice, postRequest, todayStamps +LOG = logging.getLogger(__name__) + _GETMEASURE_REQ = _BASE_URL + "api/getmeasure" _GETSTATIONDATA_REQ = _BASE_URL + "api/getstationsdata" @@ -24,7 +27,11 @@ def __init__(self, authData, urlReq=None): resp = postRequest(self.urlReq, postParams) if resp is None: raise NoDevice("No weather station data returned by Netatmo server") - self.rawData = resp["body"].get("devices") + try: + self.rawData = resp["body"].get("devices") + except KeyError: + LOG.debug("No in response %s", resp) + raise NoDevice("No weather station data returned by Netatmo server") if not self.rawData: raise NoDevice("No weather station available") self.stations = {d["_id"]: d for d in self.rawData} diff --git a/smart_home/__init__.py b/smart_home/__init__.py index fa19fbcd4..68fd2a602 100644 --- a/smart_home/__init__.py +++ b/smart_home/__init__.py @@ -21,11 +21,17 @@ class NoDevice(Exception): def postRequest(url, params=None, timeout=10): resp = requests.post(url, data=params, timeout=timeout) - return ( - resp.json() - if "application/json" in resp.headers.get("content-type") - else resp.content - ) + if not resp.ok: + LOG.error("The Netatmo API returned %s", resp.status_code) + try: + return ( + resp.json() + if "application/json" in resp.headers.get("content-type") + else resp.content + ) + except TypeError: + LOG.debug("Invalid response %s", resp) + return None def toTimeString(value): From 2c4932aa6246f320def6a88c87aabe463afe4fad Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 11 Jun 2019 10:19:32 +0200 Subject: [PATCH 073/333] Remove deprecated methods (#62) * Remove unneccessary imports * Remove deprecated thermostatdata * Remove thermostatdata class * Remove utf8 comments and deprecated class * Remote unused import --- pyatmo.py | 11 +-- smart_home/Camera.py | 3 - smart_home/HomeCoach.py | 3 - smart_home/PublicData.py | 3 - smart_home/Thermostat.py | 134 ----------------------------------- smart_home/WeatherStation.py | 15 ---- smart_home/__init__.py | 3 - 7 files changed, 7 insertions(+), 165 deletions(-) diff --git a/pyatmo.py b/pyatmo.py index fd15c1538..d86b094a2 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -7,7 +7,6 @@ cameras or/and the Netatmo smart thermostat This package can be used with Python3 applications PythonAPI Netatmo REST data access -coding=utf-8 """ import logging import time @@ -16,8 +15,8 @@ from smart_home.Camera import CameraData from smart_home.HomeCoach import HomeCoachData from smart_home.PublicData import PublicData -from smart_home.Thermostat import HomeData, HomeStatus, ThermostatData -from smart_home.WeatherStation import DeviceList, WeatherStationData +from smart_home.Thermostat import HomeData, HomeStatus +from smart_home.WeatherStation import WeatherStationData LOG = logging.getLogger(__name__) @@ -62,7 +61,11 @@ def __init__( resp = postRequest(_AUTH_REQ, postParams) self._clientId = clientId self._clientSecret = clientSecret - self._accessToken = resp["access_token"] + try: + self._accessToken = resp["access_token"] + except (KeyError): + LOG.error("Netatmo API returned %s", resp["error"]) + raise NoDevice("Authentication against Netatmo API failed") self.refreshToken = resp["refresh_token"] self._scope = resp["scope"] self.expiration = int(resp["expire_in"] + time.time() - 1800) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index d32da5731..fabcde3b2 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -1,6 +1,3 @@ -""" -coding=utf-8 -""" import imghdr import logging import time diff --git a/smart_home/HomeCoach.py b/smart_home/HomeCoach.py index 29f46fd40..a3880f8d0 100644 --- a/smart_home/HomeCoach.py +++ b/smart_home/HomeCoach.py @@ -1,6 +1,3 @@ -""" -coding=utf-8 -""" from . import _BASE_URL from .WeatherStation import WeatherStationData diff --git a/smart_home/PublicData.py b/smart_home/PublicData.py index 25eddda71..97c2c53ca 100644 --- a/smart_home/PublicData.py +++ b/smart_home/PublicData.py @@ -1,6 +1,3 @@ -""" -coding=utf-8 -""" import time from . import _BASE_URL, NoDevice, postRequest diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index d0318f3bd..6d66a39f8 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -1,6 +1,3 @@ -""" -coding=utf-8 -""" import logging import time @@ -8,8 +5,6 @@ LOG = logging.getLogger(__name__) -_SETTEMP_REQ = _BASE_URL + "api/setthermpoint" -_GETTHERMOSTATDATA_REQ = _BASE_URL + "api/getthermostatsdata" _GETHOMESDATA_REQ = _BASE_URL + "api/homesdata" _GETHOMESTATUS_REQ = _BASE_URL + "api/homestatus" _SETTHERMMODE_REQ = _BASE_URL + "api/setthermmode" @@ -321,132 +316,3 @@ def setroomThermpoint(self, home_id, room_id, mode, temp=None): if temp is not None: postParams["temp"] = temp return postRequest(_SETROOMTHERMPOINT_REQ, postParams) - - -class ThermostatData: - """ - List the Thermostat devices (relays and thermostat modules) - - Args: - authData (ClientAuth): Authentication information with a working access Token - """ - - def __init__(self, authData): - self.getAuthToken = authData.accessToken - postParams = {"access_token": self.getAuthToken} - resp = postRequest(_GETTHERMOSTATDATA_REQ, postParams) - - self.rawData = resp["body"] - self.devList = self.rawData["devices"] - if not self.devList: - raise NoDevice("No thermostat available") - self.devId = self.devList[0]["_id"] - self.modList = self.devList[0]["modules"] - self.modId = self.modList[0]["_id"] - self.temp = self.modList[0]["measured"]["temperature"] - self.setpoint_mode = self.modList[0]["setpoint"]["setpoint_mode"] - if self.setpoint_mode == "manual": - self.setpoint_temp = self.modList[0]["setpoint"]["setpoint_temp"] - else: - self.setpoint_temp = self.modList[0]["measured"]["setpoint_temp"] - self.relay_cmd = int(self.modList[0]["therm_relay_cmd"]) - self.devices = {d["_id"]: d for d in self.rawData["devices"]} - self.modules = dict() - self.therm_program_list = dict() - self.zones = dict() - self.timetable = dict() - for i in range(len(self.rawData["devices"])): - nameDevice = self.rawData["devices"][i]["station_name"] - if nameDevice not in self.modules: - self.modules[nameDevice] = dict() - for m in self.rawData["devices"][i]["modules"]: - self.modules[nameDevice][m["_id"]] = m - for p in self.rawData["devices"][i]["modules"][0]["therm_program_list"]: - self.therm_program_list[p["program_id"]] = p - for z in self.rawData["devices"][i]["modules"][0]["therm_program_list"][0][ - "zones" - ]: - self.zones[z["id"]] = z - for o in self.rawData["devices"][i]["modules"][0]["therm_program_list"][0][ - "timetable" - ]: - self.timetable[o["m_offset"]] = o - self.default_device = list(self.devices.values())[0]["station_name"] - - self.default_module = list(self.modules[self.default_device].values())[0][ - "module_name" - ] - - def lastData(self, device=None, exclude=0): - s = self.deviceByName(device) - if not s: - return None - lastD = dict() - zones = dict() - # Define oldest acceptable sensor measure event - limit = (time.time() - exclude) if exclude else 0 - dm = s["modules"][0]["measured"] - ds = s["modules"][0]["setpoint"]["setpoint_mode"] - dz = s["modules"][0]["therm_program_list"][0]["zones"] - for module in s["modules"]: - dm = module["measured"] - ds = module["setpoint"]["setpoint_mode"] - dz = module["therm_program_list"][0]["zones"] - if dm["time"] > limit: - lastD[module["module_name"]] = dm.copy() - lastD[module["module_name"]]["setpoint_mode"] = ds - # For potential use, add battery and radio coverage information to module data if present - for i in ( - "battery_vp", - "rf_status", - "therm_relay_cmd", - "battery_percent", - ): - if i in module: - lastD[module["module_name"]][i] = module[i] - zones[module["module_name"]] = dz.copy() - return lastD - - def deviceById(self, did): - return None if did not in self.devices else self.devices[did] - - def deviceByName(self, device): - if not device: - device = self.default_device - for key, value in self.devices.items(): - if value["station_name"] == device: - return self.devices[key] - - def moduleById(self, mid): - for device, mod in self.modules.items(): - if mid in self.modules[device]: - return self.modules[device][mid] - return None - - def moduleByName(self, module=None, device=None): - if not module and not device: - return self.default_module - elif device and module: - if device not in self.modules: - return None - for mod_id in self.modules[device]: - if self.modules[device][mod_id]["module_name"] == module: - return self.modules[device][mod_id] - elif not device and module: - for device, mod_ids in self.modules.items(): - for mod_id in mod_ids: - if self.modules[device][mod_id]["module_name"] == module: - return self.modules[device][mod_id] - else: - return list(self.modules[device].values())[0] - return None - - def setthermpoint(self, mode, temp, endTimeOffset): - postParams = {"access_token": self.getAuthToken} - postParams["device_id"] = self.devId - postParams["module_id"] = self.modId - postParams["setpoint_mode"] = mode - if mode == "manual": - postParams["setpoint_endtime"] = time.time() + endTimeOffset - postParams["setpoint_temp"] = temp - return postRequest(_SETTEMP_REQ, postParams) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 5b57eff26..9abb2d954 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -1,9 +1,5 @@ -""" -coding=utf-8 -""" import logging import time -import warnings from . import _BASE_URL, NoDevice, postRequest, todayStamps @@ -233,14 +229,3 @@ def MinMaxTH(self, station=None, module=None, frame="last24"): return min(T), max(T), min(H), max(H) else: return None - - -class DeviceList(WeatherStationData): - """ - This class is now deprecated. Use WeatherStationData directly instead - """ - - warnings.warn( - "The 'DeviceList' class was renamed 'WeatherStationData'", DeprecationWarning - ) - pass diff --git a/smart_home/__init__.py b/smart_home/__init__.py index 68fd2a602..d9ac53a03 100644 --- a/smart_home/__init__.py +++ b/smart_home/__init__.py @@ -1,8 +1,5 @@ -import json import logging -import socket import time -import traceback import requests From 72ffddb5c712075e8aac5884d5a132ff6b9994f0 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 11 Jun 2019 14:52:04 +0200 Subject: [PATCH 074/333] Flake8 pep8 venv (#61) * Ignore files in venv * Set line length for flake8 and pep8 --- .gitignore | 2 ++ setup.cfg | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/.gitignore b/.gitignore index 4ac39cd6d..e1473d8d1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ pyatmo.egg-info/ *.pyc .DS_Store + +venv/ \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index b88034e41..8b5c68bad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,8 @@ [metadata] description-file = README.md + +[flake8] +max-line-length = 88 + +[pep8] +max-line-length = 88 \ No newline at end of file From 3559cfe9af3e976608f7146d8788fcbcf923d6aa Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 12 Jun 2019 21:37:15 +0200 Subject: [PATCH 075/333] Remove unnecessary checks (#63) --- smart_home/Thermostat.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 6d66a39f8..158863372 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -1,5 +1,4 @@ import logging -import time from . import _BASE_URL, NoDevice, postRequest @@ -195,29 +194,21 @@ def __init__(self, authData, home_id=None, home=None): self.default_valve = list(self.valves.values())[0] def roomById(self, rid): - if not rid: - return self.default_room for key, value in self.rooms.items(): if value["id"] == rid: return self.rooms[key] def thermostatById(self, rid): - if not rid: - return self.default_thermostat for key, value in self.thermostats.items(): if value["id"] == rid: return self.thermostats[key] def relayById(self, rid): - if not rid: - return self.default_relay for key, value in self.relays.items(): if value["id"] == rid: return self.relays[key] def valveById(self, rid): - if not rid: - return self.default_valve for key, value in self.valves.items(): if value["id"] == rid: return self.valves[key] From 608620d3feae42df8372efa4922f5a306c5c0ec3 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Wed, 12 Jun 2019 21:40:31 +0200 Subject: [PATCH 076/333] v2.0.0 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 925f3fbcf..bb2380c95 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="1.12", # Should be updated with new versions + version="2.0.0", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From 4292d4eea6d603e9c314c8d0c720f1fcbdce3a02 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 14 Jun 2019 23:09:59 +0200 Subject: [PATCH 077/333] Revert "Remove unnecessary checks" (#64) * Revert "Remove unnecessary checks (#63)" This reverts commit 3559cfe9af3e976608f7146d8788fcbcf923d6aa. * Remote unnecessary time import --- smart_home/Thermostat.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 158863372..4d37da93c 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -194,21 +194,29 @@ def __init__(self, authData, home_id=None, home=None): self.default_valve = list(self.valves.values())[0] def roomById(self, rid): + if not rid: + return self.default_room for key, value in self.rooms.items(): if value["id"] == rid: return self.rooms[key] def thermostatById(self, rid): + if not rid: + return self.default_thermostat for key, value in self.thermostats.items(): if value["id"] == rid: return self.thermostats[key] def relayById(self, rid): + if not rid: + return self.default_relay for key, value in self.relays.items(): if value["id"] == rid: return self.relays[key] def valveById(self, rid): + if not rid: + return self.default_valve for key, value in self.valves.items(): if value["id"] == rid: return self.valves[key] From d4b3bfde7ebd512011853d60fd13b0b2300fa902 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sat, 15 Jun 2019 21:44:47 +0200 Subject: [PATCH 078/333] v2.0.1 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bb2380c95..73eeae9cc 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="2.0.0", # Should be updated with new versions + version="2.0.1", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From 00d010ac671b872c20add5b864025461ff198c6a Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 29 May 2019 22:37:24 +0200 Subject: [PATCH 079/333] Add tests for postRequest and ClientAuth --- fixtures/invalid_grant.json | 1 + fixtures/oauth2_token.json | 1 + tests/__init__.py | 0 tests/test_pyatmo.py | 59 +++++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 fixtures/invalid_grant.json create mode 100644 fixtures/oauth2_token.json create mode 100644 tests/__init__.py create mode 100644 tests/test_pyatmo.py diff --git a/fixtures/invalid_grant.json b/fixtures/invalid_grant.json new file mode 100644 index 000000000..2ad6a6ab9 --- /dev/null +++ b/fixtures/invalid_grant.json @@ -0,0 +1 @@ +{"error": "invalid_grant"} diff --git a/fixtures/oauth2_token.json b/fixtures/oauth2_token.json new file mode 100644 index 000000000..9ac074cef --- /dev/null +++ b/fixtures/oauth2_token.json @@ -0,0 +1 @@ +{"access_token": "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12", "refresh_token": "91763b24c43d3e344f424e8b|87ff8755a00c6b8a120b55a08c758e93", "scope": ["read_station", "read_camera", "access_camera", "read_thermostat", "write_thermostat", "read_presence", "access_presence"], "expires_in": 10800, "expire_in": 10800} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py new file mode 100644 index 000000000..181ec869a --- /dev/null +++ b/tests/test_pyatmo.py @@ -0,0 +1,59 @@ +"""Define tests for authentication.""" +import json +import pytest +import pyatmo + + +def test_postRequest(requests_mock): + """Test the wrapper for posting requests against the Netatmo API.""" + requests_mock.post( + pyatmo._BASE_URL, json={"a": "b"}, headers={"content-type": "application/json"} + ) + resp = pyatmo.postRequest(pyatmo._BASE_URL, None) + assert resp == {"a": "b"} + + requests_mock.post( + pyatmo._BASE_URL, text="Success", headers={"content-type": "application/text"} + ) + resp = pyatmo.postRequest(pyatmo._BASE_URL, None) + assert resp == b"Success" + + +def test_ClientAuth(requests_mock): + with open("fixtures/oauth2_token.json") as f: + json_fixture = json.load(f) + requests_mock.post( + pyatmo._AUTH_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + authorization = pyatmo.ClientAuth( + clientId="CLIENT_ID", + clientSecret="CLIENT_SECRET", + username="USERNAME", + password="PASSWORD", + scope="read_station read_camera access_camera " + "read_thermostat write_thermostat " + "read_presence access_presence read_homecoach", + ) + assert ( + authorization.accessToken + == "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12" + ) + + +@pytest.mark.xfail(raises=KeyError) +def test_ClientAuth_invalid(requests_mock): + with open("fixtures/invalid_grant.json") as f: + json_fixture = json.load(f) + requests_mock.post( + pyatmo._AUTH_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + authorization = pyatmo.ClientAuth( + clientId="CLIENT_ID", + clientSecret="CLIENT_SECRET", + username="USERNAME", + password="PASSWORD", + ) From a20ae4c5dae22714bd835566fac813048445c026 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 30 May 2019 01:10:15 +0200 Subject: [PATCH 080/333] Split up tests --- fixtures/status_ok.json | 1 + tests/conftest.py | 25 +++++++++++++++++++++++++ tests/test_pyatmo.py | 36 ++---------------------------------- tests/test_smart_home.py | 19 +++++++++++++++++++ 4 files changed, 47 insertions(+), 34 deletions(-) create mode 100644 fixtures/status_ok.json create mode 100644 tests/conftest.py create mode 100644 tests/test_smart_home.py diff --git a/fixtures/status_ok.json b/fixtures/status_ok.json new file mode 100644 index 000000000..e14f06021 --- /dev/null +++ b/fixtures/status_ok.json @@ -0,0 +1 @@ +{"status": "ok", "time_exec": 0.020781993865967, "time_server": 1559162635} diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..83eb5842c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,25 @@ +"""Define shared fixtures.""" +import json +import pytest +import pyatmo + + +@pytest.fixture(scope="function") +def auth(requests_mock): + with open("fixtures/oauth2_token.json") as f: + json_fixture = json.load(f) + requests_mock.post( + pyatmo._AUTH_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + authorization = pyatmo.ClientAuth( + clientId="CLIENT_ID", + clientSecret="CLIENT_SECRET", + username="USERNAME", + password="PASSWORD", + scope="read_station read_camera access_camera " + "read_thermostat write_thermostat " + "read_presence access_presence read_homecoach", + ) + return authorization diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index 181ec869a..7d8caf196 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -4,41 +4,9 @@ import pyatmo -def test_postRequest(requests_mock): - """Test the wrapper for posting requests against the Netatmo API.""" - requests_mock.post( - pyatmo._BASE_URL, json={"a": "b"}, headers={"content-type": "application/json"} - ) - resp = pyatmo.postRequest(pyatmo._BASE_URL, None) - assert resp == {"a": "b"} - - requests_mock.post( - pyatmo._BASE_URL, text="Success", headers={"content-type": "application/text"} - ) - resp = pyatmo.postRequest(pyatmo._BASE_URL, None) - assert resp == b"Success" - - -def test_ClientAuth(requests_mock): - with open("fixtures/oauth2_token.json") as f: - json_fixture = json.load(f) - requests_mock.post( - pyatmo._AUTH_REQ, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - authorization = pyatmo.ClientAuth( - clientId="CLIENT_ID", - clientSecret="CLIENT_SECRET", - username="USERNAME", - password="PASSWORD", - scope="read_station read_camera access_camera " - "read_thermostat write_thermostat " - "read_presence access_presence read_homecoach", - ) +def test_ClientAuth(auth, requests_mock): assert ( - authorization.accessToken - == "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12" + auth.accessToken == "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12" ) diff --git a/tests/test_smart_home.py b/tests/test_smart_home.py new file mode 100644 index 000000000..28fccde9b --- /dev/null +++ b/tests/test_smart_home.py @@ -0,0 +1,19 @@ +"""Define tests for untility methods.""" +import json +import pytest +import pyatmo + + +def test_postRequest(requests_mock): + """Test the wrapper for posting requests against the Netatmo API.""" + requests_mock.post( + pyatmo._BASE_URL, json={"a": "b"}, headers={"content-type": "application/json"} + ) + resp = pyatmo.postRequest(pyatmo._BASE_URL, None) + assert resp == {"a": "b"} + + requests_mock.post( + pyatmo._BASE_URL, text="Success", headers={"content-type": "application/text"} + ) + resp = pyatmo.postRequest(pyatmo._BASE_URL, None) + assert resp == b"Success" From 9088429d4de29a89e32b2e0abad87c6dfbbd58b0 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 30 May 2019 23:38:45 +0200 Subject: [PATCH 081/333] Reformat fixtures --- fixtures/home_data_simple.json | 403 +++++++++++++++++++++++++++++++++ fixtures/invalid_grant.json | 4 +- fixtures/oauth2_token.json | 16 +- fixtures/status_ok.json | 6 +- 4 files changed, 426 insertions(+), 3 deletions(-) create mode 100644 fixtures/home_data_simple.json diff --git a/fixtures/home_data_simple.json b/fixtures/home_data_simple.json new file mode 100644 index 000000000..90feab785 --- /dev/null +++ b/fixtures/home_data_simple.json @@ -0,0 +1,403 @@ +{ + "body": { + "homes": [ + { + "id": "91763b24c43d3e344f424e8b", + "name": "MYHOME", + "altitude": 112, + "coordinates": [ + 52.516263, + 13.377726 + ], + "country": "DE", + "timezone": "Europe/Berlin", + "rooms": [ + { + "id": "2746182631", + "name": "Livingroom", + "type": "livingroom", + "module_ids": [ + "12:34:56:00:01:ae" + ] + }, + { + "id": "3688132631", + "name": "Hall", + "type": "custom", + "module_ids": [ + "12:34:56:00:f1:62" + ] + } + ], + "modules": [ + { + "id": "12:34:56:00:fa:d0", + "type": "NAPlug", + "name": "Raumthermostat", + "setup_date": 1494963356, + "modules_bridged": [ + "12:34:56:00:01:ae" + ] + }, + { + "id": "12:34:56:00:01:ae", + "type": "NATherm1", + "name": "Livingroom", + "setup_date": 1494963356, + "room_id": "2746182631", + "bridge": "12:34:56:00:fa:d0" + }, + { + "id": "12:34:56:00:f1:62", + "type": "NACamera", + "name": "Hall", + "setup_date": 1544828430, + "room_id": "3688132631" + } + ], + "therm_schedules": [ + { + "zones": [ + { + "type": 0, + "name": "Comfort", + "rooms_temp": [ + { + "temp": 21, + "room_id": "2746182631" + } + ], + "id": 0 + }, + { + "type": 1, + "name": "Night", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 1 + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 4 + } + ], + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 360 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 960 + }, + { + "zone_id": 1, + "m_offset": 1410 + }, + { + "zone_id": 0, + "m_offset": 1800 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2400 + }, + { + "zone_id": 1, + "m_offset": 2850 + }, + { + "zone_id": 0, + "m_offset": 3240 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3840 + }, + { + "zone_id": 1, + "m_offset": 4290 + }, + { + "zone_id": 0, + "m_offset": 4680 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5280 + }, + { + "zone_id": 1, + "m_offset": 5730 + }, + { + "zone_id": 0, + "m_offset": 6120 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6720 + }, + { + "zone_id": 1, + "m_offset": 7170 + }, + { + "zone_id": 0, + "m_offset": 7620 + }, + { + "zone_id": 1, + "m_offset": 8610 + }, + { + "zone_id": 0, + "m_offset": 9060 + }, + { + "zone_id": 1, + "m_offset": 10050 + } + ], + "hg_temp": 7, + "away_temp": 14, + "name": "Default", + "id": "591b54a2764ff4d50d8b5795", + "type": "therm" + } + ], + "therm_setpoint_default_duration": 120, + "persons": [ + { + "id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "pseudo": "John Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7" + }, + { + "id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "pseudo": "Jane Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff3", + "pseudo": "Richard Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" + } + ], + "schedules": [ + { + "zones": [ + { + "type": 0, + "name": "Komfort", + "rooms_temp": [ + { + "temp": 21, + "room_id": "2746182631" + } + ], + "id": 0, + "rooms": [ + { + "id": "2746182631", + "therm_setpoint_temperature": 21 + } + ] + }, + { + "type": 1, + "name": "Nacht", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 1, + "rooms": [ + { + "id": "2746182631", + "therm_setpoint_temperature": 17 + } + ] + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 4, + "rooms": [ + { + "id": "2746182631", + "therm_setpoint_temperature": 17 + } + ] + } + ], + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 360 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 960 + }, + { + "zone_id": 1, + "m_offset": 1410 + }, + { + "zone_id": 0, + "m_offset": 1800 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2400 + }, + { + "zone_id": 1, + "m_offset": 2850 + }, + { + "zone_id": 0, + "m_offset": 3240 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3840 + }, + { + "zone_id": 1, + "m_offset": 4290 + }, + { + "zone_id": 0, + "m_offset": 4680 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5280 + }, + { + "zone_id": 1, + "m_offset": 5730 + }, + { + "zone_id": 0, + "m_offset": 6120 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6720 + }, + { + "zone_id": 1, + "m_offset": 7170 + }, + { + "zone_id": 0, + "m_offset": 7620 + }, + { + "zone_id": 1, + "m_offset": 8610 + }, + { + "zone_id": 0, + "m_offset": 9060 + }, + { + "zone_id": 1, + "m_offset": 10050 + } + ], + "hg_temp": 7, + "away_temp": 14, + "name": "Default", + "id": "591b54a2764ff4d50d8b5795", + "type": "therm" + } + ], + "therm_mode": "schedule" + } + ], + "user": { + "email": "john@doe.com", + "language": "de-DE", + "locale": "de-DE", + "feel_like_algorithm": 0, + "unit_pressure": 0, + "unit_system": 0, + "unit_wind": 0, + "id": "91763b24c43d3e344f424e8b" + } + }, + "status": "ok", + "time_exec": 0.056135892868042, + "time_server": 1559171003 +} \ No newline at end of file diff --git a/fixtures/invalid_grant.json b/fixtures/invalid_grant.json index 2ad6a6ab9..5d2f1fda9 100644 --- a/fixtures/invalid_grant.json +++ b/fixtures/invalid_grant.json @@ -1 +1,3 @@ -{"error": "invalid_grant"} +{ + "error": "invalid_grant" +} \ No newline at end of file diff --git a/fixtures/oauth2_token.json b/fixtures/oauth2_token.json index 9ac074cef..444af4f46 100644 --- a/fixtures/oauth2_token.json +++ b/fixtures/oauth2_token.json @@ -1 +1,15 @@ -{"access_token": "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12", "refresh_token": "91763b24c43d3e344f424e8b|87ff8755a00c6b8a120b55a08c758e93", "scope": ["read_station", "read_camera", "access_camera", "read_thermostat", "write_thermostat", "read_presence", "access_presence"], "expires_in": 10800, "expire_in": 10800} +{ + "access_token": "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12", + "refresh_token": "91763b24c43d3e344f424e8b|87ff8755a00c6b8a120b55a08c758e93", + "scope": [ + "read_station", + "read_camera", + "access_camera", + "read_thermostat", + "write_thermostat", + "read_presence", + "access_presence" + ], + "expires_in": 10800, + "expire_in": 10800 +} \ No newline at end of file diff --git a/fixtures/status_ok.json b/fixtures/status_ok.json index e14f06021..672749493 100644 --- a/fixtures/status_ok.json +++ b/fixtures/status_ok.json @@ -1 +1,5 @@ -{"status": "ok", "time_exec": 0.020781993865967, "time_server": 1559162635} +{ + "status": "ok", + "time_exec": 0.020781993865967, + "time_server": 1559162635 +} \ No newline at end of file From dc46cc9fec33c53f6314b5d5ef2fddb90e400c6f Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 31 May 2019 00:42:28 +0200 Subject: [PATCH 082/333] Add more thermostat tests --- fixtures/home_data_simple.json | 2 ++ tests/conftest.py | 13 +++++++ tests/test_smart_home_thermostat.py | 55 +++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 tests/test_smart_home_thermostat.py diff --git a/fixtures/home_data_simple.json b/fixtures/home_data_simple.json index 90feab785..fadcf2cdb 100644 --- a/fixtures/home_data_simple.json +++ b/fixtures/home_data_simple.json @@ -197,6 +197,7 @@ "hg_temp": 7, "away_temp": 14, "name": "Default", + "selected": true, "id": "591b54a2764ff4d50d8b5795", "type": "therm" } @@ -380,6 +381,7 @@ "away_temp": 14, "name": "Default", "id": "591b54a2764ff4d50d8b5795", + "selected": true, "type": "therm" } ], diff --git a/tests/conftest.py b/tests/conftest.py index 83eb5842c..a28d97f45 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ import json import pytest import pyatmo +import smart_home.Thermostat as th @pytest.fixture(scope="function") @@ -23,3 +24,15 @@ def auth(requests_mock): "read_presence access_presence read_homecoach", ) return authorization + + +@pytest.fixture(scope="function") +def homeData(auth, requests_mock): + with open("fixtures/home_data_simple.json") as f: + json_fixture = json.load(f) + requests_mock.post( + th._GETHOMESDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + return pyatmo.HomeData(auth) diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py new file mode 100644 index 000000000..f64b6f501 --- /dev/null +++ b/tests/test_smart_home_thermostat.py @@ -0,0 +1,55 @@ +"""Define tests for Thermostat module.""" +import json +import pytest +import pyatmo +import smart_home.Thermostat as th + + +def test_HomeData(auth, homeData, requests_mock): + assert homeData.default_home == "MYHOME" + assert len(homeData.rooms[homeData.default_home]) == 2 + + assert len(homeData.modules[homeData.default_home]) == 3 + + expected_modules = { + "12:34:56:00:fa:d0": { + "id": "12:34:56:00:fa:d0", + "type": "NAPlug", + "name": "Raumthermostat", + "setup_date": 1494963356, + "modules_bridged": ["12:34:56:00:01:ae"], + }, + "12:34:56:00:01:ae": { + "id": "12:34:56:00:01:ae", + "type": "NATherm1", + "name": "Livingroom", + "setup_date": 1494963356, + "room_id": "2746182631", + "bridge": "12:34:56:00:fa:d0", + }, + "12:34:56:00:f1:62": { + "id": "12:34:56:00:f1:62", + "type": "NACamera", + "name": "Hall", + "setup_date": 1544828430, + "room_id": "3688132631", + }, + } + assert homeData.modules[homeData.default_home] == expected_modules + + +def test_HomeData_homeById(auth, homeData, requests_mock): + home_id = "91763b24c43d3e344f424e8b" + assert homeData.homeById(home_id)["name"] == "MYHOME" + + +def test_HomeData_homeByName(auth, homeData, requests_mock): + assert homeData.homeByName()["name"] == "MYHOME" + + +def test_HomeData_gethomeId(auth, homeData, requests_mock): + assert homeData.gethomeId() == "91763b24c43d3e344f424e8b" + + +def test_HomeData_getSelectedschedule(auth, homeData, requests_mock): + assert homeData.getSelectedschedule()["name"] == "Default" From 2cb1a377f137b1f810b33730de98ae72dac45159 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 31 May 2019 08:48:34 +0200 Subject: [PATCH 083/333] Add tests for utility methods --- tests/conftest.py | 2 + tests/test_pyatmo.py | 2 + tests/test_smart_home.py | 70 ++++++++++++++++++++++++++--- tests/test_smart_home_thermostat.py | 4 -- 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a28d97f45..fa558aa73 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,8 @@ """Define shared fixtures.""" import json + import pytest + import pyatmo import smart_home.Thermostat as th diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index 7d8caf196..2c1bba467 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -1,6 +1,8 @@ """Define tests for authentication.""" import json + import pytest + import pyatmo diff --git a/tests/test_smart_home.py b/tests/test_smart_home.py index 28fccde9b..56cd474bc 100644 --- a/tests/test_smart_home.py +++ b/tests/test_smart_home.py @@ -1,19 +1,75 @@ """Define tests for untility methods.""" -import json +import time + import pytest -import pyatmo + +import smart_home def test_postRequest(requests_mock): - """Test the wrapper for posting requests against the Netatmo API.""" + """Test wrapper for posting requests against the Netatmo API.""" requests_mock.post( - pyatmo._BASE_URL, json={"a": "b"}, headers={"content-type": "application/json"} + smart_home._BASE_URL, + json={"a": "b"}, + headers={"content-type": "application/json"}, ) - resp = pyatmo.postRequest(pyatmo._BASE_URL, None) + resp = smart_home.postRequest(smart_home._BASE_URL, None) assert resp == {"a": "b"} requests_mock.post( - pyatmo._BASE_URL, text="Success", headers={"content-type": "application/text"} + smart_home._BASE_URL, + text="Success", + headers={"content-type": "application/text"}, ) - resp = pyatmo.postRequest(pyatmo._BASE_URL, None) + resp = smart_home.postRequest(smart_home._BASE_URL, None) assert resp == b"Success" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (1, "1970-01-01_01:00:01"), + (0, "1970-01-01_01:00:00"), + (-1, "1970-01-01_00:59:59"), + (2000000000, "2033-05-18_05:33:20"), + ("1", "1970-01-01_01:00:01"), + pytest.param("A", None, marks=pytest.mark.xfail), + pytest.param([1], None, marks=pytest.mark.xfail), + pytest.param({1}, None, marks=pytest.mark.xfail), + ], +) +def test_toTimeString(test_input, expected): + """Test time to string conversion.""" + assert smart_home.toTimeString(test_input) == expected + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("1970-01-01_01:00:01", 1), + ("1970-01-01_01:00:00", 0), + ("1970-01-01_00:59:59", -1), + ("2033-05-18_05:33:20", 2000000000), + ], +) +def test_toEpoch(test_input, expected): + """Test time to epoch conversion.""" + assert smart_home.toEpoch(test_input) == expected + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("2018-06-21", (1529532000, 1529618400)), + ("2000-01-01", (946681200, 946767600)), + pytest.param("2000-04-31", None, marks=pytest.mark.xfail), + ], +) +def test_todayStamps(monkeypatch, test_input, expected): + """Test todayStamps function.""" + + def mockreturn(format): + return test_input + + monkeypatch.setattr(time, "strftime", mockreturn) + assert smart_home.todayStamps() == expected diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index f64b6f501..8be3e1620 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -1,8 +1,4 @@ """Define tests for Thermostat module.""" -import json -import pytest -import pyatmo -import smart_home.Thermostat as th def test_HomeData(auth, homeData, requests_mock): From 8005f8e76cb49453d5c323bbd5486b0373bbe4c7 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 31 May 2019 08:58:58 +0200 Subject: [PATCH 084/333] Clean up --- tests/test_smart_home_thermostat.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index 8be3e1620..69b4bb436 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -1,7 +1,7 @@ """Define tests for Thermostat module.""" -def test_HomeData(auth, homeData, requests_mock): +def test_HomeData(homeData): assert homeData.default_home == "MYHOME" assert len(homeData.rooms[homeData.default_home]) == 2 @@ -34,18 +34,18 @@ def test_HomeData(auth, homeData, requests_mock): assert homeData.modules[homeData.default_home] == expected_modules -def test_HomeData_homeById(auth, homeData, requests_mock): +def test_HomeData_homeById(homeData): home_id = "91763b24c43d3e344f424e8b" assert homeData.homeById(home_id)["name"] == "MYHOME" -def test_HomeData_homeByName(auth, homeData, requests_mock): +def test_HomeData_homeByName(homeData): assert homeData.homeByName()["name"] == "MYHOME" -def test_HomeData_gethomeId(auth, homeData, requests_mock): +def test_HomeData_gethomeId(homeData): assert homeData.gethomeId() == "91763b24c43d3e344f424e8b" -def test_HomeData_getSelectedschedule(auth, homeData, requests_mock): +def test_HomeData_getSelectedschedule(homeData): assert homeData.getSelectedschedule()["name"] == "Default" From 3377932b6d5fc283908b61b4e1ca51680f1786c9 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 31 May 2019 09:01:07 +0200 Subject: [PATCH 085/333] Fix linter --- tests/test_pyatmo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index 2c1bba467..ee1a49443 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -7,8 +7,8 @@ def test_ClientAuth(auth, requests_mock): - assert ( - auth.accessToken == "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12" + assert auth.accessToken == ( + "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12" ) @@ -21,7 +21,7 @@ def test_ClientAuth_invalid(requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - authorization = pyatmo.ClientAuth( + pyatmo.ClientAuth( clientId="CLIENT_ID", clientSecret="CLIENT_SECRET", username="USERNAME", From 707ea82464438c5f5b08c1cb81ae1afbc7ca5e11 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 31 May 2019 12:58:12 +0200 Subject: [PATCH 086/333] Add tests for home status --- fixtures/home_status_simple.json | 69 ++++++++++++++++++++++ tests/conftest.py | 19 ++++++ tests/test_smart_home_thermostat.py | 89 ++++++++++++++++++++++++++++- 3 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 fixtures/home_status_simple.json diff --git a/fixtures/home_status_simple.json b/fixtures/home_status_simple.json new file mode 100644 index 000000000..487176c93 --- /dev/null +++ b/fixtures/home_status_simple.json @@ -0,0 +1,69 @@ +{ + "status": "ok", + "time_server": 1559292039, + "body": { + "home": { + "modules": [ + { + "id": "12:34:56:00:f1:62", + "type": "NACamera", + "monitoring": "on", + "sd_status": 4, + "alim_status": 2, + "locked": false, + "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.123.45/609e27de5699fb18147ab47d06846631/MTRPn_BeWCav5RBq4U1OMDruTW4dkQ0NuMwNDAw11g,,", + "is_local": true + }, + { + "id": "12:34:56:00:fa:d0", + "type": "NAPlug", + "firmware_revision": 174, + "rf_strength": 107, + "wifi_strength": 42 + }, + { + "id": "12:34:56:00:01:ae", + "reachable": true, + "type": "NATherm1", + "firmware_revision": 65, + "rf_strength": 58, + "battery_level": 3793, + "boiler_valve_comfort_boost": false, + "boiler_status": false, + "anticipating": false, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "high" + } + ], + "rooms": [ + { + "id": "2746182631", + "reachable": true, + "therm_measured_temperature": 19.8, + "therm_setpoint_temperature": 12, + "therm_setpoint_mode": "away", + "therm_setpoint_start_time": 1559229567, + "therm_setpoint_end_time": 0 + } + ], + "id": "91763b24c43d3e344f424e8b", + "persons": [ + { + "id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "last_seen": 1557071156, + "out_of_sight": true + }, + { + "id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "last_seen": 1559282761, + "out_of_sight": false + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff3", + "last_seen": 1559224132, + "out_of_sight": true + } + ] + } + } +} \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index fa558aa73..1a27313da 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,3 +38,22 @@ def homeData(auth, requests_mock): headers={"content-type": "application/json"}, ) return pyatmo.HomeData(auth) + + +@pytest.fixture(scope="function") +def homeStatus(auth, requests_mock): + with open("fixtures/home_status_simple.json") as f: + json_fixture = json.load(f) + requests_mock.post( + th._GETHOMESTATUS_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with open("fixtures/home_data_simple.json") as f: + json_fixture = json.load(f) + requests_mock.post( + th._GETHOMESDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + return pyatmo.HomeStatus(auth) diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index 69b4bb436..006c0f42d 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -7,7 +7,7 @@ def test_HomeData(homeData): assert len(homeData.modules[homeData.default_home]) == 3 - expected_modules = { + expected = { "12:34:56:00:fa:d0": { "id": "12:34:56:00:fa:d0", "type": "NAPlug", @@ -31,7 +31,7 @@ def test_HomeData(homeData): "room_id": "3688132631", }, } - assert homeData.modules[homeData.default_home] == expected_modules + assert homeData.modules[homeData.default_home] == expected def test_HomeData_homeById(homeData): @@ -49,3 +49,88 @@ def test_HomeData_gethomeId(homeData): def test_HomeData_getSelectedschedule(homeData): assert homeData.getSelectedschedule()["name"] == "Default" + + +def test_HomeStatus(homeStatus): + assert len(homeStatus.rooms) == 1 + assert homeStatus.default_room["id"] == "2746182631" + + expexted = { + "id": "2746182631", + "reachable": True, + "therm_measured_temperature": 19.8, + "therm_setpoint_temperature": 12, + "therm_setpoint_mode": "away", + "therm_setpoint_start_time": 1559229567, + "therm_setpoint_end_time": 0, + } + assert homeStatus.default_room == expexted + + +def test_HomeStatus_roomById(homeStatus): + expexted = { + "id": "2746182631", + "reachable": True, + "therm_measured_temperature": 19.8, + "therm_setpoint_temperature": 12, + "therm_setpoint_mode": "away", + "therm_setpoint_start_time": 1559229567, + "therm_setpoint_end_time": 0, + } + assert homeStatus.roomById("2746182631") == expexted + + +def test_HomeStatus_thermostatById(homeStatus): + expexted = { + "id": "12:34:56:00:01:ae", + "reachable": True, + "type": "NATherm1", + "firmware_revision": 65, + "rf_strength": 58, + "battery_level": 3793, + "boiler_valve_comfort_boost": False, + "boiler_status": False, + "anticipating": False, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "high", + } + assert homeStatus.thermostatById("12:34:56:00:01:ae") == expexted + + +def test_HomeStatus_relayById(homeStatus): + expexted = { + "id": "12:34:56:00:fa:d0", + "type": "NAPlug", + "firmware_revision": 174, + "rf_strength": 107, + "wifi_strength": 42, + } + assert homeStatus.relayById("12:34:56:00:fa:d0") == expexted + + +def test_HomeStatus_setPoint(homeStatus): + assert homeStatus.setPoint("2746182631") == 12 + + +def test_HomeStatus_setPointmode(homeStatus): + assert homeStatus.setPointmode("2746182631") == "away" + + +def test_HomeStatus_getAwaytemp(homeStatus): + assert homeStatus.getAwaytemp() == 14 + + +def test_HomeStatus_getHgtemp(homeStatus): + assert homeStatus.getHgtemp() == 7 + + +def test_HomeStatus_measuredTemperature(homeStatus): + assert homeStatus.measuredTemperature() == 19.8 + + +def test_HomeStatus_boilerStatus(homeStatus): + assert homeStatus.boilerStatus() == False + + +def test_HomeStatus_thermostatType(homeStatus, homeData): + assert homeStatus.thermostatType("MYHOME", "2746182631") == "NATherm1" From af8e385dd93066d48d39428d98642948965fcdac Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 31 May 2019 14:03:06 +0200 Subject: [PATCH 087/333] Add tests for thermostatData --- fixtures/home_data_simple.json | 2 +- tests/conftest.py | 12 +++++++ tests/test_smart_home_thermostat.py | 53 ++++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/fixtures/home_data_simple.json b/fixtures/home_data_simple.json index fadcf2cdb..f1d2f57f0 100644 --- a/fixtures/home_data_simple.json +++ b/fixtures/home_data_simple.json @@ -33,7 +33,7 @@ { "id": "12:34:56:00:fa:d0", "type": "NAPlug", - "name": "Raumthermostat", + "name": "Thermostat", "setup_date": 1494963356, "modules_bridged": [ "12:34:56:00:01:ae" diff --git a/tests/conftest.py b/tests/conftest.py index 1a27313da..63027f040 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,3 +57,15 @@ def homeStatus(auth, requests_mock): headers={"content-type": "application/json"}, ) return pyatmo.HomeStatus(auth) + + +@pytest.fixture(scope="function") +def thermostatData(auth, requests_mock): + with open("fixtures/thermostat_data_simple.json") as f: + json_fixture = json.load(f) + requests_mock.post( + th._GETTHERMOSTATDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + return pyatmo.ThermostatData(auth) diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index 006c0f42d..370a245bf 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -1,4 +1,5 @@ """Define tests for Thermostat module.""" +import pytest def test_HomeData(homeData): @@ -11,7 +12,7 @@ def test_HomeData(homeData): "12:34:56:00:fa:d0": { "id": "12:34:56:00:fa:d0", "type": "NAPlug", - "name": "Raumthermostat", + "name": "Thermostat", "setup_date": 1494963356, "modules_bridged": ["12:34:56:00:01:ae"], }, @@ -134,3 +135,53 @@ def test_HomeStatus_boilerStatus(homeStatus): def test_HomeStatus_thermostatType(homeStatus, homeData): assert homeStatus.thermostatType("MYHOME", "2746182631") == "NATherm1" + + +def test_ThermostatData(thermostatData): + assert thermostatData.default_device == "Thermostat" + assert thermostatData.default_module == "Livingroom" + assert thermostatData.temp == 19.8 + + +def test_ThermostatData_lastData(thermostatData): + expected = { + "Livingroom": { + "time": 1559297836, + "temperature": 19.8, + "setpoint_temp": 12, + "setpoint_mode": "away", + "battery_vp": 3798, + "rf_status": 59, + "therm_relay_cmd": 0, + "battery_percent": 53, + } + } + assert thermostatData.lastData() == expected + + +@pytest.mark.parametrize( + "test_input,expected", [("station_name", "Thermostat"), ("type", "NAPlug")] +) +def test_ThermostatData_deviceById(thermostatData, test_input, expected): + assert thermostatData.deviceById("12:34:56:00:fa:d0")[test_input] == expected + + +@pytest.mark.parametrize( + "test_input,expected", [("_id", "12:34:56:00:fa:d0"), ("type", "NAPlug")] +) +def test_ThermostatData_deviceByName(thermostatData, test_input, expected): + assert thermostatData.deviceByName("Thermostat")[test_input] == expected + + +@pytest.mark.parametrize( + "test_input,expected", [("module_name", "Livingroom"), ("type", "NATherm1")] +) +def test_ThermostatData_moduleById(thermostatData, test_input, expected): + assert thermostatData.moduleById("12:34:56:00:01:ae")[test_input] == expected + + +@pytest.mark.parametrize( + "test_input,expected", [("_id", "12:34:56:00:01:ae"), ("type", "NATherm1")] +) +def test_ThermostatData_moduleByName(thermostatData, test_input, expected): + assert thermostatData.moduleByName("Livingroom")[test_input] == expected From 477f81e462ff1691af663f8073670acf4348384b Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 31 May 2019 14:53:00 +0200 Subject: [PATCH 088/333] Add test for setthermpoint --- fixtures/thermostat_data_simple.json | 268 +++++++++++++++++++++++++++ tests/test_smart_home_thermostat.py | 26 +++ 2 files changed, 294 insertions(+) create mode 100644 fixtures/thermostat_data_simple.json diff --git a/fixtures/thermostat_data_simple.json b/fixtures/thermostat_data_simple.json new file mode 100644 index 000000000..cda81b1ea --- /dev/null +++ b/fixtures/thermostat_data_simple.json @@ -0,0 +1,268 @@ +{ + "body": { + "devices": [ + { + "_id": "12:34:56:00:fa:d0", + "firmware": 174, + "last_bilan": { + "y": 2019, + "m": 4 + }, + "last_setup": 1494963356, + "last_status_store": 1559297986, + "place": { + "altitude": 112, + "city": "Berlin", + "country": "DE", + "improveLocProposed": true, + "location": [ + 52.516263, + 13.377726 + ], + "timezone": "Europe/Berlin", + "trust_location": true + }, + "plug_connected_boiler": 1, + "type": "NAPlug", + "udp_conn": true, + "wifi_status": 42, + "modules": [ + { + "_id": "12:34:56:00:01:ae", + "module_name": "Livingroom", + "type": "NATherm1", + "firmware": 65, + "last_message": 1559297976, + "rf_status": 59, + "battery_vp": 3798, + "therm_orientation": 3, + "therm_relay_cmd": 0, + "anticipating": false, + "battery_percent": 53, + "event_history": { + "boiler_not_responding_events": [ + { + "K": 1506103090 + }, + { + "K": 1514496738 + }, + { + "K": 1514583682 + }, + { + "K": 1518695843 + }, + { + "K": 1518813960 + } + ], + "boiler_responding_events": [ + { + "K": 1506281109 + }, + { + "K": 1514552830 + }, + { + "K": 1514757686 + }, + { + "K": 1518798339 + }, + { + "K": 1518965265 + } + ] + }, + "setpoint_history": [ + { + "setpoint": { + "setpoint_mode": "hg" + }, + "timestamp": 1559229554 + }, + { + "setpoint": { + "setpoint_mode": "program" + }, + "timestamp": 1559229565 + }, + { + "setpoint": { + "setpoint_mode": "away" + }, + "timestamp": 1559229567 + } + ], + "last_therm_seen": 1559297976, + "setpoint": { + "setpoint_mode": "away" + }, + "therm_program_list": [ + { + "zones": [ + { + "type": 0, + "name": "Comfort", + "id": 0, + "temp": 21 + }, + { + "type": 1, + "name": "Night", + "id": 1, + "temp": 17 + }, + { + "type": 5, + "name": "Eco", + "id": 4, + "temp": 17 + }, + { + "type": 2, + "id": 2, + "temp": 14 + }, + { + "type": 3, + "id": 3, + "temp": 7 + } + ], + "timetable": [ + { + "m_offset": 0, + "id": 1 + }, + { + "m_offset": 360, + "id": 0 + }, + { + "m_offset": 420, + "id": 4 + }, + { + "m_offset": 960, + "id": 0 + }, + { + "m_offset": 1410, + "id": 1 + }, + { + "m_offset": 1800, + "id": 0 + }, + { + "m_offset": 1860, + "id": 4 + }, + { + "m_offset": 2400, + "id": 0 + }, + { + "m_offset": 2850, + "id": 1 + }, + { + "m_offset": 3240, + "id": 0 + }, + { + "m_offset": 3300, + "id": 4 + }, + { + "m_offset": 3840, + "id": 0 + }, + { + "m_offset": 4290, + "id": 1 + }, + { + "m_offset": 4680, + "id": 0 + }, + { + "m_offset": 4740, + "id": 4 + }, + { + "m_offset": 5280, + "id": 0 + }, + { + "m_offset": 5730, + "id": 1 + }, + { + "m_offset": 6120, + "id": 0 + }, + { + "m_offset": 6180, + "id": 4 + }, + { + "m_offset": 6720, + "id": 0 + }, + { + "m_offset": 7170, + "id": 1 + }, + { + "m_offset": 7620, + "id": 0 + }, + { + "m_offset": 8610, + "id": 1 + }, + { + "m_offset": 9060, + "id": 0 + }, + { + "m_offset": 10050, + "id": 1 + } + ], + "name": "Default", + "program_id": "591b54a2764ff4d50d8b5795", + "selected": true + } + ], + "measured": { + "time": 1559297836, + "temperature": 19.8, + "setpoint_temp": 12 + } + } + ], + "station_name": "Thermostat", + "last_plug_seen": 1559297986 + } + ], + "user": { + "mail": "john@doe.com", + "administrative": { + "lang": "de-DE", + "reg_locale": "de-DE", + "country": "DE", + "unit": 0, + "windunit": 0, + "pressureunit": 0, + "feel_like_algo": 0 + } + } + }, + "status": "ok", + "time_exec": 0.12061500549316, + "time_server": 1559300497 +} \ No newline at end of file diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index 370a245bf..4a596f58f 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -1,6 +1,10 @@ """Define tests for Thermostat module.""" +import json + import pytest +import smart_home.Thermostat as th + def test_HomeData(homeData): assert homeData.default_home == "MYHOME" @@ -185,3 +189,25 @@ def test_ThermostatData_moduleById(thermostatData, test_input, expected): ) def test_ThermostatData_moduleByName(thermostatData, test_input, expected): assert thermostatData.moduleByName("Livingroom")[test_input] == expected + + +@pytest.mark.parametrize( + "mode, temp, endTimeOffset, expected", + [ + ( + "manual", + 19, + 3600, + {"status": "ok", "time_exec": 0.020781993865967, "time_server": 1559162635}, + ) + ], +) +def test_ThermostatData_setthermpoint( + thermostatData, requests_mock, mode, temp, endTimeOffset, expected +): + with open("fixtures/status_ok.json") as f: + json_fixture = json.load(f) + requests_mock.post( + th._SETTEMP_REQ, json=json_fixture, headers={"content-type": "application/json"} + ) + assert thermostatData.setthermpoint(mode, temp, endTimeOffset) == expected From 267ff30c0d14ba6081ba9d28eb6fded660b15373 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 4 Jun 2019 08:11:06 +0200 Subject: [PATCH 089/333] Add test for invalid response --- tests/test_smart_home.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/test_smart_home.py b/tests/test_smart_home.py index 56cd474bc..6712bdda6 100644 --- a/tests/test_smart_home.py +++ b/tests/test_smart_home.py @@ -6,7 +6,7 @@ import smart_home -def test_postRequest(requests_mock): +def test_postRequest_json(requests_mock): """Test wrapper for posting requests against the Netatmo API.""" requests_mock.post( smart_home._BASE_URL, @@ -16,6 +16,9 @@ def test_postRequest(requests_mock): resp = smart_home.postRequest(smart_home._BASE_URL, None) assert resp == {"a": "b"} + +def test_postRequest_binary(requests_mock): + """Test wrapper for posting requests against the Netatmo API.""" requests_mock.post( smart_home._BASE_URL, text="Success", @@ -25,6 +28,16 @@ def test_postRequest(requests_mock): assert resp == b"Success" +def test_postRequest_invalid_response(requests_mock): + """Test wrapper for posting requests against the Netatmo API.""" + requests_mock.post( + smart_home._BASE_URL, + status_code=200, + ) + resp = smart_home.postRequest(smart_home._BASE_URL, None) + assert resp is None + + @pytest.mark.parametrize( "test_input,expected", [ From 1025dba4b14a7b07859e22d460ea2b0d453135f2 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 4 Jun 2019 08:31:06 +0200 Subject: [PATCH 090/333] Add test for failing connection --- tests/test_smart_home.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/test_smart_home.py b/tests/test_smart_home.py index 6712bdda6..9bedc41fd 100644 --- a/tests/test_smart_home.py +++ b/tests/test_smart_home.py @@ -28,14 +28,12 @@ def test_postRequest_binary(requests_mock): assert resp == b"Success" -def test_postRequest_invalid_response(requests_mock): - """Test wrapper for posting requests against the Netatmo API.""" - requests_mock.post( - smart_home._BASE_URL, - status_code=200, - ) +@pytest.mark.parametrize("test_input,expected", [(200, None), (404, None)]) +def test_postRequest_fail(requests_mock, test_input, expected): + """Test failing requests against the Netatmo API.""" + requests_mock.post(smart_home._BASE_URL, status_code=test_input) resp = smart_home.postRequest(smart_home._BASE_URL, None) - assert resp is None + assert resp is expected @pytest.mark.parametrize( From 895681dd233d1169e3b62087d316b4bc4acb0d9d Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 4 Jun 2019 15:35:58 +0200 Subject: [PATCH 091/333] Add tests for public weather function --- fixtures/public_data_error_mongo.json | 6 ++ fixtures/public_data_simple.json | 58 ++++++++++++++++++++ fixtures/too_many_connections.json | 3 + tests/conftest.py | 10 ++-- tests/test_pyatmo.py | 2 +- tests/test_smart_home_thermostat.py | 79 +-------------------------- tests/test_smarthome_publicdata.py | 36 ++++++++++++ 7 files changed, 110 insertions(+), 84 deletions(-) create mode 100644 fixtures/public_data_error_mongo.json create mode 100644 fixtures/public_data_simple.json create mode 100644 fixtures/too_many_connections.json create mode 100644 tests/test_smarthome_publicdata.py diff --git a/fixtures/public_data_error_mongo.json b/fixtures/public_data_error_mongo.json new file mode 100644 index 000000000..d99e5487c --- /dev/null +++ b/fixtures/public_data_error_mongo.json @@ -0,0 +1,6 @@ +{ + "error": { + "message": "failed to connect to server [localhost:27020] on first connect [MongoError: connect ECONNREFUSED 127.0.0.1:27020]", + "code": 0 + } +} \ No newline at end of file diff --git a/fixtures/public_data_simple.json b/fixtures/public_data_simple.json new file mode 100644 index 000000000..d63ecc447 --- /dev/null +++ b/fixtures/public_data_simple.json @@ -0,0 +1,58 @@ +{ + "status": "ok", + "time_server": 1559649219, + "time_exec": 0, + "body": [ + { + "_id": "70:ee:50:19:d7:0e", + "place": { + "location": [ + 52.516263, + 13.377726 + ], + "timezone": "Europe/Zurich", + "country": "CH", + "altitude": 1025 + }, + "mark": 1, + "measures": { + "02:00:00:19:e3:b6": { + "res": { + "1559648910": [ + 28.5, + 33 + ] + }, + "type": [ + "temperature", + "humidity" + ] + }, + "70:ee:50:19:d7:0e": { + "res": { + "1559648915": [ + 1008.2 + ] + }, + "type": [ + "pressure" + ] + }, + "05:00:00:02:37:90": { + "rain_60min": 0, + "rain_24h": 0, + "rain_live": 0, + "rain_timeutc": 1559648904 + } + }, + "modules": [ + "02:00:00:19:e3:b6", + "05:00:00:02:37:90" + ], + "module_types": { + "02:00:00:19:e3:b6": "NAModule1", + "05:00:00:02:37:90": "NAModule3" + } + } + ] +} \ No newline at end of file diff --git a/fixtures/too_many_connections.json b/fixtures/too_many_connections.json new file mode 100644 index 000000000..b8f8a4711 --- /dev/null +++ b/fixtures/too_many_connections.json @@ -0,0 +1,3 @@ +{ + "error": "too_many_connections" +} \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 63027f040..7baab75f7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ import pytest import pyatmo -import smart_home.Thermostat as th +import smart_home.Thermostat @pytest.fixture(scope="function") @@ -33,7 +33,7 @@ def homeData(auth, requests_mock): with open("fixtures/home_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - th._GETHOMESDATA_REQ, + smart_home.Thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) @@ -45,14 +45,14 @@ def homeStatus(auth, requests_mock): with open("fixtures/home_status_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - th._GETHOMESTATUS_REQ, + smart_home.Thermostat._GETHOMESTATUS_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) with open("fixtures/home_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - th._GETHOMESDATA_REQ, + smart_home.Thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) @@ -64,7 +64,7 @@ def thermostatData(auth, requests_mock): with open("fixtures/thermostat_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - th._GETTHERMOSTATDATA_REQ, + smart_home.Thermostat._GETTHERMOSTATDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index ee1a49443..855eb7666 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -12,7 +12,7 @@ def test_ClientAuth(auth, requests_mock): ) -@pytest.mark.xfail(raises=KeyError) +@pytest.mark.xfail(raises=pyatmo.NoDevice) def test_ClientAuth_invalid(requests_mock): with open("fixtures/invalid_grant.json") as f: json_fixture = json.load(f) diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index 4a596f58f..7bb082320 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -1,9 +1,4 @@ """Define tests for Thermostat module.""" -import json - -import pytest - -import smart_home.Thermostat as th def test_HomeData(homeData): @@ -134,80 +129,8 @@ def test_HomeStatus_measuredTemperature(homeStatus): def test_HomeStatus_boilerStatus(homeStatus): - assert homeStatus.boilerStatus() == False + assert homeStatus.boilerStatus() is False def test_HomeStatus_thermostatType(homeStatus, homeData): assert homeStatus.thermostatType("MYHOME", "2746182631") == "NATherm1" - - -def test_ThermostatData(thermostatData): - assert thermostatData.default_device == "Thermostat" - assert thermostatData.default_module == "Livingroom" - assert thermostatData.temp == 19.8 - - -def test_ThermostatData_lastData(thermostatData): - expected = { - "Livingroom": { - "time": 1559297836, - "temperature": 19.8, - "setpoint_temp": 12, - "setpoint_mode": "away", - "battery_vp": 3798, - "rf_status": 59, - "therm_relay_cmd": 0, - "battery_percent": 53, - } - } - assert thermostatData.lastData() == expected - - -@pytest.mark.parametrize( - "test_input,expected", [("station_name", "Thermostat"), ("type", "NAPlug")] -) -def test_ThermostatData_deviceById(thermostatData, test_input, expected): - assert thermostatData.deviceById("12:34:56:00:fa:d0")[test_input] == expected - - -@pytest.mark.parametrize( - "test_input,expected", [("_id", "12:34:56:00:fa:d0"), ("type", "NAPlug")] -) -def test_ThermostatData_deviceByName(thermostatData, test_input, expected): - assert thermostatData.deviceByName("Thermostat")[test_input] == expected - - -@pytest.mark.parametrize( - "test_input,expected", [("module_name", "Livingroom"), ("type", "NATherm1")] -) -def test_ThermostatData_moduleById(thermostatData, test_input, expected): - assert thermostatData.moduleById("12:34:56:00:01:ae")[test_input] == expected - - -@pytest.mark.parametrize( - "test_input,expected", [("_id", "12:34:56:00:01:ae"), ("type", "NATherm1")] -) -def test_ThermostatData_moduleByName(thermostatData, test_input, expected): - assert thermostatData.moduleByName("Livingroom")[test_input] == expected - - -@pytest.mark.parametrize( - "mode, temp, endTimeOffset, expected", - [ - ( - "manual", - 19, - 3600, - {"status": "ok", "time_exec": 0.020781993865967, "time_server": 1559162635}, - ) - ], -) -def test_ThermostatData_setthermpoint( - thermostatData, requests_mock, mode, temp, endTimeOffset, expected -): - with open("fixtures/status_ok.json") as f: - json_fixture = json.load(f) - requests_mock.post( - th._SETTEMP_REQ, json=json_fixture, headers={"content-type": "application/json"} - ) - assert thermostatData.setthermpoint(mode, temp, endTimeOffset) == expected diff --git a/tests/test_smarthome_publicdata.py b/tests/test_smarthome_publicdata.py new file mode 100644 index 000000000..bf85a87fa --- /dev/null +++ b/tests/test_smarthome_publicdata.py @@ -0,0 +1,36 @@ +"""Define tests for Public weather module.""" +import json + +import pytest + +import smart_home.PublicData + + +def test_PublicData(auth, requests_mock): + with open("fixtures/public_data_simple.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.PublicData._GETPUBLIC_DATA, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + publicData = smart_home.PublicData.PublicData(auth) + assert publicData.status == "ok" + + +@pytest.mark.xfail(raises=smart_home.PublicData.NoDevice) +def test_PublicData_unavailable(auth, requests_mock): + requests_mock.post(smart_home.PublicData._GETPUBLIC_DATA, status_code=404) + smart_home.PublicData.PublicData(auth) + + +@pytest.mark.xfail(raises=smart_home.PublicData.NoDevice) +def test_PublicData_error(auth, requests_mock): + with open("fixtures/public_data_error_mongo.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.PublicData._GETPUBLIC_DATA, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + smart_home.PublicData.PublicData(auth) From 5a842b1c29060de80a60c6b13813d00c18e1d654 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 4 Jun 2019 15:40:58 +0200 Subject: [PATCH 092/333] Rename file --- ...test_smarthome_publicdata.py => test_smart_home_publicdata.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_smarthome_publicdata.py => test_smart_home_publicdata.py} (100%) diff --git a/tests/test_smarthome_publicdata.py b/tests/test_smart_home_publicdata.py similarity index 100% rename from tests/test_smarthome_publicdata.py rename to tests/test_smart_home_publicdata.py From 0eb801fe95a822a7c709f39a97c667b77d44effc Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 8 Jun 2019 00:04:05 +0200 Subject: [PATCH 093/333] Assert exceptions instead of marking the functions --- tests/test_pyatmo.py | 14 +++++++------- tests/test_smart_home_publicdata.py | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index 855eb7666..dc8c7846f 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -12,7 +12,6 @@ def test_ClientAuth(auth, requests_mock): ) -@pytest.mark.xfail(raises=pyatmo.NoDevice) def test_ClientAuth_invalid(requests_mock): with open("fixtures/invalid_grant.json") as f: json_fixture = json.load(f) @@ -21,9 +20,10 @@ def test_ClientAuth_invalid(requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - pyatmo.ClientAuth( - clientId="CLIENT_ID", - clientSecret="CLIENT_SECRET", - username="USERNAME", - password="PASSWORD", - ) + with pytest.raises(pyatmo.NoDevice): + pyatmo.ClientAuth( + clientId="CLIENT_ID", + clientSecret="CLIENT_SECRET", + username="USERNAME", + password="PASSWORD", + ) diff --git a/tests/test_smart_home_publicdata.py b/tests/test_smart_home_publicdata.py index bf85a87fa..39c33a0ef 100644 --- a/tests/test_smart_home_publicdata.py +++ b/tests/test_smart_home_publicdata.py @@ -18,13 +18,12 @@ def test_PublicData(auth, requests_mock): assert publicData.status == "ok" -@pytest.mark.xfail(raises=smart_home.PublicData.NoDevice) def test_PublicData_unavailable(auth, requests_mock): requests_mock.post(smart_home.PublicData._GETPUBLIC_DATA, status_code=404) - smart_home.PublicData.PublicData(auth) + with pytest.raises(smart_home.PublicData.NoDevice): + smart_home.PublicData.PublicData(auth) -@pytest.mark.xfail(raises=smart_home.PublicData.NoDevice) def test_PublicData_error(auth, requests_mock): with open("fixtures/public_data_error_mongo.json") as f: json_fixture = json.load(f) @@ -33,4 +32,5 @@ def test_PublicData_error(auth, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - smart_home.PublicData.PublicData(auth) + with pytest.raises(smart_home.PublicData.NoDevice): + smart_home.PublicData.PublicData(auth) From 7a4fbb3b4a6dd15b157873d9411894a28e8e41c6 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 11 Jun 2019 15:04:59 +0200 Subject: [PATCH 094/333] Add more tests for public weather data --- fixtures/public_data_simple.json | 376 ++++++++++++++++++++++++++-- tests/conftest.py | 13 + tests/test_smart_home_publicdata.py | 266 ++++++++++++++++++++ 3 files changed, 634 insertions(+), 21 deletions(-) diff --git a/fixtures/public_data_simple.json b/fixtures/public_data_simple.json index d63ecc447..552027138 100644 --- a/fixtures/public_data_simple.json +++ b/fixtures/public_data_simple.json @@ -1,26 +1,222 @@ { "status": "ok", - "time_server": 1559649219, + "time_server": 1560248397, "time_exec": 0, "body": [ { - "_id": "70:ee:50:19:d7:0e", + "_id": "70:ee:50:36:94:7c", "place": { "location": [ - 52.516263, - 13.377726 + 8.791382999999996, + 50.2136394 ], - "timezone": "Europe/Zurich", - "country": "CH", - "altitude": 1025 + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 132 + }, + "mark": 14, + "measures": { + "02:00:00:36:f2:94": { + "res": { + "1560248022": [ + 21.4, + 62 + ] + }, + "type": [ + "temperature", + "humidity" + ] + }, + "70:ee:50:36:94:7c": { + "res": { + "1560248030": [ + 1010.6 + ] + }, + "type": [ + "pressure" + ] + }, + "05:00:00:05:33:84": { + "rain_60min": 0.2, + "rain_24h": 12.322000000000001, + "rain_live": 0.5, + "rain_timeutc": 1560248022 + } + }, + "modules": [ + "05:00:00:05:33:84", + "02:00:00:36:f2:94" + ], + "module_types": { + "05:00:00:05:33:84": "NAModule3", + "02:00:00:36:f2:94": "NAModule1" + } + }, + { + "_id": "70:ee:50:1f:68:9e", + "place": { + "location": [ + 8.795445200000017, + 50.2130169 + ], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 125 + }, + "mark": 14, + "measures": { + "02:00:00:1f:82:28": { + "res": { + "1560248312": [ + 21.1, + 69 + ] + }, + "type": [ + "temperature", + "humidity" + ] + }, + "70:ee:50:1f:68:9e": { + "res": { + "1560248344": [ + 1007.3 + ] + }, + "type": [ + "pressure" + ] + }, + "05:00:00:02:bb:6e": { + "rain_60min": 0, + "rain_24h": 9.999, + "rain_live": 0, + "rain_timeutc": 1560248344 + } + }, + "modules": [ + "02:00:00:1f:82:28", + "05:00:00:02:bb:6e" + ], + "module_types": { + "02:00:00:1f:82:28": "NAModule1", + "05:00:00:02:bb:6e": "NAModule3" + } + }, + { + "_id": "70:ee:50:27:25:b0", + "place": { + "location": [ + 8.7807159, + 50.1946167 + ], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 112 + }, + "mark": 14, + "measures": { + "02:00:00:27:19:b2": { + "res": { + "1560247889": [ + 23.2, + 60 + ] + }, + "type": [ + "temperature", + "humidity" + ] + }, + "70:ee:50:27:25:b0": { + "res": { + "1560247907": [ + 1012.8 + ] + }, + "type": [ + "pressure" + ] + }, + "05:00:00:03:5d:2e": { + "rain_60min": 0, + "rain_24h": 11.716000000000001, + "rain_live": 0, + "rain_timeutc": 1560247896 + } + }, + "modules": [ + "02:00:00:27:19:b2", + "05:00:00:03:5d:2e" + ], + "module_types": { + "02:00:00:27:19:b2": "NAModule1", + "05:00:00:03:5d:2e": "NAModule3" + } + }, + { + "_id": "70:ee:50:04:ed:7a", + "place": { + "location": [ + 8.785034, + 50.192169 + ], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 112 + }, + "mark": 14, + "measures": { + "02:00:00:04:c2:2e": { + "res": { + "1560248137": [ + 19.8, + 76 + ] + }, + "type": [ + "temperature", + "humidity" + ] + }, + "70:ee:50:04:ed:7a": { + "res": { + "1560248152": [ + 1005.4 + ] + }, + "type": [ + "pressure" + ] + } + }, + "modules": [ + "02:00:00:04:c2:2e" + ], + "module_types": { + "02:00:00:04:c2:2e": "NAModule1" + } + }, + { + "_id": "70:ee:50:27:9f:2c", + "place": { + "location": [ + 8.785342, + 50.193573 + ], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 116 }, "mark": 1, "measures": { - "02:00:00:19:e3:b6": { + "02:00:00:27:aa:70": { "res": { - "1559648910": [ - 28.5, - 33 + "1560247821": [ + 25.5, + 56 ] }, "type": [ @@ -28,30 +224,168 @@ "humidity" ] }, - "70:ee:50:19:d7:0e": { + "70:ee:50:27:9f:2c": { "res": { - "1559648915": [ - 1008.2 + "1560247853": [ + 1010.6 ] }, "type": [ "pressure" ] + } + }, + "modules": [ + "02:00:00:27:aa:70" + ], + "module_types": { + "02:00:00:27:aa:70": "NAModule1" + } + }, + { + "_id": "70:ee:50:01:20:fa", + "place": { + "location": [ + 8.7953, + 50.195241 + ], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 119 + }, + "mark": 1, + "measures": { + "02:00:00:00:f7:ba": { + "res": { + "1560247831": [ + 27.4, + 58 + ] + }, + "type": [ + "temperature", + "humidity" + ] }, - "05:00:00:02:37:90": { + "70:ee:50:01:20:fa": { + "res": { + "1560247876": [ + 1014.4 + ] + }, + "type": [ + "pressure" + ] + } + }, + "modules": [ + "02:00:00:00:f7:ba" + ], + "module_types": { + "02:00:00:00:f7:ba": "NAModule1" + } + }, + { + "_id": "70:ee:50:3c:02:78", + "place": { + "location": [ + 8.795953681700666, + 50.19530139868166 + ], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 119 + }, + "mark": 7, + "measures": { + "02:00:00:3c:21:f2": { + "res": { + "1560248225": [ + 23.3, + 58 + ] + }, + "type": [ + "temperature", + "humidity" + ] + }, + "70:ee:50:3c:02:78": { + "res": { + "1560248270": [ + 1011.7 + ] + }, + "type": [ + "pressure" + ] + } + }, + "modules": [ + "02:00:00:3c:21:f2" + ], + "module_types": { + "02:00:00:3c:21:f2": "NAModule1" + } + }, + { + "_id": "70:ee:50:36:a9:fc", + "place": { + "location": [ + 8.801164269110814, + 50.19596181704958 + ], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 113 + }, + "mark": 14, + "measures": { + "02:00:00:36:a9:50": { + "res": { + "1560248145": [ + 20.1, + 67 + ] + }, + "type": [ + "temperature", + "humidity" + ] + }, + "70:ee:50:36:a9:fc": { + "res": { + "1560248191": [ + 1010 + ] + }, + "type": [ + "pressure" + ] + }, + "05:00:00:02:92:82": { "rain_60min": 0, - "rain_24h": 0, + "rain_24h": 11.009, "rain_live": 0, - "rain_timeutc": 1559648904 + "rain_timeutc": 1560248184 + }, + "06:00:00:03:19:76": { + "wind_strength": 15, + "wind_angle": 17, + "gust_strength": 31, + "gust_angle": 217, + "wind_timeutc": 1560248190 } }, "modules": [ - "02:00:00:19:e3:b6", - "05:00:00:02:37:90" + "05:00:00:02:92:82", + "02:00:00:36:a9:50", + "06:00:00:03:19:76" ], "module_types": { - "02:00:00:19:e3:b6": "NAModule1", - "05:00:00:02:37:90": "NAModule3" + "05:00:00:02:92:82": "NAModule3", + "02:00:00:36:a9:50": "NAModule1", + "06:00:00:03:19:76": "NAModule2" } } ] diff --git a/tests/conftest.py b/tests/conftest.py index 7baab75f7..c61b30d3d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ import pyatmo import smart_home.Thermostat +import smart_home.PublicData @pytest.fixture(scope="function") @@ -69,3 +70,15 @@ def thermostatData(auth, requests_mock): headers={"content-type": "application/json"}, ) return pyatmo.ThermostatData(auth) + + +@pytest.fixture(scope="function") +def publicData(auth, requests_mock): + with open("fixtures/public_data_simple.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.PublicData._GETPUBLIC_DATA, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + return pyatmo.PublicData(auth) diff --git a/tests/test_smart_home_publicdata.py b/tests/test_smart_home_publicdata.py index 39c33a0ef..b6f85fc33 100644 --- a/tests/test_smart_home_publicdata.py +++ b/tests/test_smart_home_publicdata.py @@ -34,3 +34,269 @@ def test_PublicData_error(auth, requests_mock): ) with pytest.raises(smart_home.PublicData.NoDevice): smart_home.PublicData.PublicData(auth) + + +def test_PublicData_CountStationInArea(publicData): + assert publicData.CountStationInArea() == 8 + + +def test_PublicData_getLatestRain(publicData): + expected = { + "70:ee:50:1f:68:9e": 0, + "70:ee:50:27:25:b0": 0, + "70:ee:50:36:94:7c": 0.5, + "70:ee:50:36:a9:fc": 0, + } + assert publicData.getLatestRain() == expected + assert publicData.getLive() == expected + + +def test_PublicData_getAverageRain(publicData): + assert publicData.getAverageRain() == 0.125 + + +def test_PublicData_get60minRain(publicData): + expected = { + "70:ee:50:1f:68:9e": 0, + "70:ee:50:27:25:b0": 0, + "70:ee:50:36:94:7c": 0.2, + "70:ee:50:36:a9:fc": 0, + } + assert publicData.get60min() == expected + assert publicData.get60minRain() == expected + + +def test_PublicData_getAverage60minRain(publicData): + assert publicData.getAverage60minRain() == 0.05 + + +def test_PublicData_get24hRain(publicData): + expected = { + "70:ee:50:1f:68:9e": 9.999, + "70:ee:50:27:25:b0": 11.716000000000001, + "70:ee:50:36:94:7c": 12.322000000000001, + "70:ee:50:36:a9:fc": 11.009, + } + assert publicData.get24h() == expected + assert publicData.get24hRain() == expected + + +def test_PublicData_getAverage24hRain(publicData): + assert publicData.getAverage24hRain() == 11.261500000000002 + + +def test_PublicData_getLatestPressures(publicData): + expected = { + "70:ee:50:1f:68:9e": 1007.3, + "70:ee:50:27:25:b0": 1012.8, + "70:ee:50:36:94:7c": 1010.6, + "70:ee:50:36:a9:fc": 1010, + "70:ee:50:01:20:fa": 1014.4, + "70:ee:50:04:ed:7a": 1005.4, + "70:ee:50:27:9f:2c": 1010.6, + "70:ee:50:3c:02:78": 1011.7, + } + assert publicData.getLatestPressures() == expected + + +def test_PublicData_getAveragePressure(publicData): + assert publicData.getAveragePressure() == 1010.3499999999999 + + +def test_PublicData_getLatestTemperatures(publicData): + expected = { + "70:ee:50:1f:68:9e": 21.1, + "70:ee:50:27:25:b0": 23.2, + "70:ee:50:36:94:7c": 21.4, + "70:ee:50:36:a9:fc": 20.1, + "70:ee:50:01:20:fa": 27.4, + "70:ee:50:04:ed:7a": 19.8, + "70:ee:50:27:9f:2c": 25.5, + "70:ee:50:3c:02:78": 23.3, + } + assert publicData.getLatestTemperatures() == expected + + +def test_PublicData_getAverageTemperature(publicData): + assert publicData.getAverageTemperature() == 22.725 + + +def test_PublicData_getLatestHumidities(publicData): + expected = { + "70:ee:50:1f:68:9e": 69, + "70:ee:50:27:25:b0": 60, + "70:ee:50:36:94:7c": 62, + "70:ee:50:36:a9:fc": 67, + "70:ee:50:01:20:fa": 58, + "70:ee:50:04:ed:7a": 76, + "70:ee:50:27:9f:2c": 56, + "70:ee:50:3c:02:78": 58, + } + assert publicData.getLatestHumidities() == expected + + +def test_PublicData_getAverageHumidity(publicData): + assert publicData.getAverageHumidity() == 63.25 + + +def test_PublicData_getLatestWindStrengths(publicData): + expected = {"70:ee:50:36:a9:fc": 15} + assert publicData.getLatestWindStrengths() == expected + + +def test_PublicData_getAverageWindStrength(publicData): + assert publicData.getAverageWindStrength() == 15 + + +def test_PublicData_getLatestWindAngles(publicData): + expected = {"70:ee:50:36:a9:fc": 17} + assert publicData.getLatestWindAngles() == expected + + +def test_PublicData_getLatestGustStrengths(publicData): + expected = {"70:ee:50:36:a9:fc": 31} + assert publicData.getLatestGustStrengths() == expected + + +def test_PublicData_getAverageGustStrength(publicData): + assert publicData.getAverageGustStrength() == 31 + + +def test_PublicData_getLatestGustAngles(publicData): + expected = {"70:ee:50:36:a9:fc": 217} + assert publicData.getLatestGustAngles() == expected + + +def test_PublicData_getLocations(publicData): + expected = { + "70:ee:50:1f:68:9e": [8.795445200000017, 50.2130169], + "70:ee:50:27:25:b0": [8.7807159, 50.1946167], + "70:ee:50:36:94:7c": [8.791382999999996, 50.2136394], + "70:ee:50:36:a9:fc": [8.801164269110814, 50.19596181704958], + "70:ee:50:01:20:fa": [8.7953, 50.195241], + "70:ee:50:04:ed:7a": [8.785034, 50.192169], + "70:ee:50:27:9f:2c": [8.785342, 50.193573], + "70:ee:50:3c:02:78": [8.795953681700666, 50.19530139868166], + } + assert publicData.getLocations() == expected + + +def test_PublicData_getTimeforMeasure(publicData): + expected = { + "70:ee:50:36:a9:fc": 1560248184, + "70:ee:50:1f:68:9e": 1560248344, + "70:ee:50:27:25:b0": 1560247896, + "70:ee:50:36:94:7c": 1560248022, + } + assert publicData.getTimeforMeasure() == expected + assert publicData.getTimeForRainMeasures() == expected + + +def test_PublicData_getTimeForWindMeasures(publicData): + expected = {"70:ee:50:36:a9:fc": 1560248190} + assert publicData.getTimeForWindMeasures() == expected + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ( + "pressure", + { + "70:ee:50:01:20:fa": 1014.4, + "70:ee:50:04:ed:7a": 1005.4, + "70:ee:50:1f:68:9e": 1007.3, + "70:ee:50:27:25:b0": 1012.8, + "70:ee:50:27:9f:2c": 1010.6, + "70:ee:50:36:94:7c": 1010.6, + "70:ee:50:36:a9:fc": 1010, + "70:ee:50:3c:02:78": 1011.7, + }, + ), + ( + "temperature", + { + "70:ee:50:01:20:fa": 27.4, + "70:ee:50:04:ed:7a": 19.8, + "70:ee:50:1f:68:9e": 21.1, + "70:ee:50:27:25:b0": 23.2, + "70:ee:50:27:9f:2c": 25.5, + "70:ee:50:36:94:7c": 21.4, + "70:ee:50:36:a9:fc": 20.1, + "70:ee:50:3c:02:78": 23.3, + }, + ), + ( + "humidity", + { + "70:ee:50:01:20:fa": 58, + "70:ee:50:04:ed:7a": 76, + "70:ee:50:1f:68:9e": 69, + "70:ee:50:27:25:b0": 60, + "70:ee:50:27:9f:2c": 56, + "70:ee:50:36:94:7c": 62, + "70:ee:50:36:a9:fc": 67, + "70:ee:50:3c:02:78": 58, + }, + ), + ], +) +def test_PublicData_getLatestStationMeasures(publicData, test_input, expected): + assert publicData.getLatestStationMeasures(test_input) == expected + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("wind_strength", {"70:ee:50:36:a9:fc": 15}), + ("wind_angle", {"70:ee:50:36:a9:fc": 17}), + ("gust_strength", {"70:ee:50:36:a9:fc": 31}), + ("gust_angle", {"70:ee:50:36:a9:fc": 217}), + ("wind_timeutc", {"70:ee:50:36:a9:fc": 1560248190}), + ], +) +def test_PublicData_getAccessoryMeasures(publicData, test_input, expected): + assert publicData.getAccessoryMeasures(test_input) == expected + + +@pytest.mark.parametrize( + "test_input,expected", + [(1560248190, "2019-06-11_12:16:30"), (0, "1970-01-01_01:00:00")], +) +def test_PublicData_toTimeString(test_input, expected): + assert smart_home.PublicData.toTimeString(test_input) == expected + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ( + { + "70:ee:50:01:20:fa": 1014.4, + "70:ee:50:04:ed:7a": 1005.4, + "70:ee:50:1f:68:9e": 1007.3, + "70:ee:50:27:25:b0": 1012.8, + "70:ee:50:27:9f:2c": 1010.6, + "70:ee:50:36:94:7c": 1010.6, + "70:ee:50:36:a9:fc": 1010, + "70:ee:50:3c:02:78": 1011.7, + }, + 1010.35, + ), + ( + { + "70:ee:50:01:20:fa": 27.4, + "70:ee:50:04:ed:7a": 19.8, + "70:ee:50:1f:68:9e": 21.1, + "70:ee:50:27:25:b0": 23.2, + "70:ee:50:27:9f:2c": 25.5, + "70:ee:50:36:94:7c": 21.4, + "70:ee:50:36:a9:fc": 20.1, + "70:ee:50:3c:02:78": 23.3, + }, + 22.725, + ), + ], +) +def test_PublicData_averageMeasure(test_input, expected): + assert smart_home.PublicData.averageMeasure(test_input) == expected From 60f3c9a84467ad2da03d15f1d23dbeca318840ea Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 11 Jun 2019 16:28:05 +0200 Subject: [PATCH 095/333] Remove ThermostatData test setup --- tests/conftest.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c61b30d3d..ee33d6b21 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -60,18 +60,6 @@ def homeStatus(auth, requests_mock): return pyatmo.HomeStatus(auth) -@pytest.fixture(scope="function") -def thermostatData(auth, requests_mock): - with open("fixtures/thermostat_data_simple.json") as f: - json_fixture = json.load(f) - requests_mock.post( - smart_home.Thermostat._GETTHERMOSTATDATA_REQ, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - return pyatmo.ThermostatData(auth) - - @pytest.fixture(scope="function") def publicData(auth, requests_mock): with open("fixtures/public_data_simple.json") as f: From 7291140c815ad3fe4e2af8e0d07dfb3c2253f0e3 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 11 Jun 2019 22:23:53 +0200 Subject: [PATCH 096/333] Remove argument --- tests/test_pyatmo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index dc8c7846f..40d5e57e9 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -6,7 +6,7 @@ import pyatmo -def test_ClientAuth(auth, requests_mock): +def test_ClientAuth(auth): assert auth.accessToken == ( "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12" ) From f477258c033b3ed1830dacc4728ee57494c34da1 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 11 Jun 2019 23:48:44 +0200 Subject: [PATCH 097/333] Add another test --- tests/test_smart_home_publicdata.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_smart_home_publicdata.py b/tests/test_smart_home_publicdata.py index b6f85fc33..da32032ec 100644 --- a/tests/test_smart_home_publicdata.py +++ b/tests/test_smart_home_publicdata.py @@ -17,6 +17,11 @@ def test_PublicData(auth, requests_mock): publicData = smart_home.PublicData.PublicData(auth) assert publicData.status == "ok" + publicData = smart_home.PublicData.PublicData( + auth, required_data_type="temperature,rain_live" + ) + assert publicData.status == "ok" + def test_PublicData_unavailable(auth, requests_mock): requests_mock.post(smart_home.PublicData._GETPUBLIC_DATA, status_code=404) @@ -296,6 +301,7 @@ def test_PublicData_toTimeString(test_input, expected): }, 22.725, ), + ({}, 0), ], ) def test_PublicData_averageMeasure(test_input, expected): From 8f35d0fa85403153fe2acb7b46d1950d6c06af14 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 12 Jun 2019 00:20:19 +0200 Subject: [PATCH 098/333] Add more tests --- fixtures/error_scope.json | 1 + fixtures/home_data_empty.json | 6 ++++++ tests/test_smart_home_thermostat.py | 23 +++++++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 fixtures/error_scope.json create mode 100644 fixtures/home_data_empty.json diff --git a/fixtures/error_scope.json b/fixtures/error_scope.json new file mode 100644 index 000000000..cfbbb8e1f --- /dev/null +++ b/fixtures/error_scope.json @@ -0,0 +1 @@ +{"error":{"code":13,"message":"Application does not have the good scope rights"}} diff --git a/fixtures/home_data_empty.json b/fixtures/home_data_empty.json new file mode 100644 index 000000000..b929cf6ee --- /dev/null +++ b/fixtures/home_data_empty.json @@ -0,0 +1,6 @@ +{ + "body": {}, + "status": "ok", + "time_exec": 0.056135892868042, + "time_server": 1559171003 +} \ No newline at end of file diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index 7bb082320..0c3e1bb2e 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -1,4 +1,9 @@ """Define tests for Thermostat module.""" +import json + +import pytest + +import smart_home.Thermostat def test_HomeData(homeData): @@ -34,6 +39,24 @@ def test_HomeData(homeData): assert homeData.modules[homeData.default_home] == expected +def test_HomeData_noData(auth, requests_mock): + requests_mock.post(smart_home.Thermostat._GETHOMESDATA_REQ, text="None") + with pytest.raises(smart_home.PublicData.NoDevice): + assert smart_home.Thermostat.HomeData(auth) + + +def test_HomeData_noBody(auth, requests_mock): + with open("fixtures/home_data_empty.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Thermostat._GETHOMESDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with pytest.raises(smart_home.PublicData.NoDevice): + assert smart_home.Thermostat.HomeData(auth) + + def test_HomeData_homeById(homeData): home_id = "91763b24c43d3e344f424e8b" assert homeData.homeById(home_id)["name"] == "MYHOME" From 4c2b7d87392234417825f1d96df99b9c7f0e1992 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 12 Jun 2019 15:37:47 +0200 Subject: [PATCH 099/333] Add test for timeout error --- tests/test_smart_home.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_smart_home.py b/tests/test_smart_home.py index 9bedc41fd..37dff3372 100644 --- a/tests/test_smart_home.py +++ b/tests/test_smart_home.py @@ -3,6 +3,8 @@ import pytest +import requests + import smart_home @@ -36,6 +38,13 @@ def test_postRequest_fail(requests_mock, test_input, expected): assert resp is expected +def test_postRequest_timeout(requests_mock): + """Test failing requests against the Netatmo API with timeouts.""" + requests_mock.post(smart_home._BASE_URL, exc=requests.exceptions.ConnectTimeout) + with pytest.raises(requests.exceptions.ConnectTimeout): + assert smart_home.postRequest(smart_home._BASE_URL, None) + + @pytest.mark.parametrize( "test_input,expected", [ From 09020a0642a3609d6025ea45946ffe4ca1c942fb Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 12 Jun 2019 16:26:00 +0200 Subject: [PATCH 100/333] Add test for schedule switch --- tests/test_smart_home_thermostat.py | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index 0c3e1bb2e..6039a9256 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -5,6 +5,13 @@ import smart_home.Thermostat +from contextlib import contextmanager + + +@contextmanager +def does_not_raise(): + yield + def test_HomeData(homeData): assert homeData.default_home == "MYHOME" @@ -57,6 +64,18 @@ def test_HomeData_noBody(auth, requests_mock): assert smart_home.Thermostat.HomeData(auth) +def test_HomeData_noHomeName(auth, requests_mock): + with open("fixtures/home_data_nohomename.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Thermostat._GETHOMESDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with pytest.raises(smart_home.PublicData.NoDevice): + assert smart_home.Thermostat.HomeData(auth) + + def test_HomeData_homeById(homeData): home_id = "91763b24c43d3e344f424e8b" assert homeData.homeById(home_id)["name"] == "MYHOME" @@ -74,6 +93,30 @@ def test_HomeData_getSelectedschedule(homeData): assert homeData.getSelectedschedule()["name"] == "Default" +@pytest.mark.parametrize( + "t_home, t_sched_id, t_sched, expected", + [ + (None, None, None, pytest.raises(smart_home.Thermostat.NoSchedule)), + (None, None, "Default", does_not_raise()), + (None, "591b54a2764ff4d50d8b5795", None, does_not_raise()), + ], +) +def test_HomeData_switchHomeSchedule( + homeData, requests_mock, t_home, t_sched_id, t_sched, expected +): + with open("fixtures/status_ok.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Thermostat._SWITCHHOMESCHEDULE_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with expected: + homeData.switchHomeSchedule( + schedule_id=t_sched_id, schedule=t_sched, home=t_home + ) + + def test_HomeStatus(homeStatus): assert len(homeStatus.rooms) == 1 assert homeStatus.default_room["id"] == "2746182631" From ba1f2da546947c19edcc2b5e11ad366b3965d3bc Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 12 Jun 2019 20:45:21 +0200 Subject: [PATCH 101/333] Add valves to fixtures --- fixtures/home_data_simple.json | 181 +++++++++++++++++++++++++++- fixtures/home_status_simple.json | 44 +++++++ tests/test_smart_home_thermostat.py | 49 +++++++- 3 files changed, 269 insertions(+), 5 deletions(-) diff --git a/fixtures/home_data_simple.json b/fixtures/home_data_simple.json index f1d2f57f0..abfcae61f 100644 --- a/fixtures/home_data_simple.json +++ b/fixtures/home_data_simple.json @@ -27,6 +27,22 @@ "module_ids": [ "12:34:56:00:f1:62" ] + }, + { + "id": "2833524037", + "name": "Entrada", + "type": "lobby", + "module_ids": [ + "12:34:56:03:a5:54" + ] + }, + { + "id": "2940411577", + "name": "Cocina", + "type": "kitchen", + "module_ids": [ + "12:34:56:03:a0:ac" + ] } ], "modules": [ @@ -36,7 +52,9 @@ "name": "Thermostat", "setup_date": 1494963356, "modules_bridged": [ - "12:34:56:00:01:ae" + "12:34:56:00:01:ae", + "12:34:56:03:a0:ac", + "12:34:56:03:a5:54" ] }, { @@ -47,6 +65,22 @@ "room_id": "2746182631", "bridge": "12:34:56:00:fa:d0" }, + { + "id": "12:34:56:03:a5:54", + "type": "NRV", + "name": "Valve1", + "setup_date": 1554549767, + "room_id": "2833524037", + "bridge": "12:34:56:00:fa:d0" + }, + { + "id": "12:34:56:03:a0:ac", + "type": "NRV", + "name": "Valve2", + "setup_date": 1554554444, + "room_id": "2940411577", + "bridge": "12:34:56:00:fa:d0" + }, { "id": "12:34:56:00:f1:62", "type": "NACamera", @@ -200,6 +234,151 @@ "selected": true, "id": "591b54a2764ff4d50d8b5795", "type": "therm" + }, + { + "zones": [ + { + "type": 0, + "name": "Comfort", + "rooms_temp": [ + { + "temp": 21, + "room_id": "2746182631" + } + ], + "id": 0 + }, + { + "type": 1, + "name": "Night", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 1 + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 4 + } + ], + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 360 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 960 + }, + { + "zone_id": 1, + "m_offset": 1410 + }, + { + "zone_id": 0, + "m_offset": 1800 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2400 + }, + { + "zone_id": 1, + "m_offset": 2850 + }, + { + "zone_id": 0, + "m_offset": 3240 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3840 + }, + { + "zone_id": 1, + "m_offset": 4290 + }, + { + "zone_id": 0, + "m_offset": 4680 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5280 + }, + { + "zone_id": 1, + "m_offset": 5730 + }, + { + "zone_id": 0, + "m_offset": 6120 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6720 + }, + { + "zone_id": 1, + "m_offset": 7170 + }, + { + "zone_id": 0, + "m_offset": 7620 + }, + { + "zone_id": 1, + "m_offset": 8610 + }, + { + "zone_id": 0, + "m_offset": 9060 + }, + { + "zone_id": 1, + "m_offset": 10050 + } + ], + "hg_temp": 7, + "away_temp": 14, + "name": "Winter", + "selected": true, + "id": "b1b54a2f45795764f59d50d8", + "type": "therm" } ], "therm_setpoint_default_duration": 120, diff --git a/fixtures/home_status_simple.json b/fixtures/home_status_simple.json index 487176c93..98c16f8d2 100644 --- a/fixtures/home_status_simple.json +++ b/fixtures/home_status_simple.json @@ -33,6 +33,26 @@ "anticipating": false, "bridge": "12:34:56:00:fa:d0", "battery_state": "high" + }, + { + "id": "12:34:56:03:a5:54", + "reachable": true, + "type": "NRV", + "firmware_revision": 79, + "rf_strength": 51, + "battery_level": 3025, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "full" + }, + { + "id": "12:34:56:03:a0:ac", + "reachable": true, + "type": "NRV", + "firmware_revision": 79, + "rf_strength": 59, + "battery_level": 3029, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "full" } ], "rooms": [ @@ -44,6 +64,30 @@ "therm_setpoint_mode": "away", "therm_setpoint_start_time": 1559229567, "therm_setpoint_end_time": 0 + }, + { + "id": "2940411577", + "reachable": true, + "therm_measured_temperature": 27, + "heating_power_request": 0, + "therm_setpoint_temperature": 7, + "therm_setpoint_mode": "hg", + "therm_setpoint_start_time": 0, + "therm_setpoint_end_time": 0, + "anticipating": false, + "open_window": false + }, + { + "id": "2833524037", + "reachable": true, + "therm_measured_temperature": 24.5, + "heating_power_request": 0, + "therm_setpoint_temperature": 7, + "therm_setpoint_mode": "hg", + "therm_setpoint_start_time": 0, + "therm_setpoint_end_time": 0, + "anticipating": false, + "open_window": false } ], "id": "91763b24c43d3e344f424e8b", diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index 6039a9256..cbd90bafe 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -15,9 +15,9 @@ def does_not_raise(): def test_HomeData(homeData): assert homeData.default_home == "MYHOME" - assert len(homeData.rooms[homeData.default_home]) == 2 + assert len(homeData.rooms[homeData.default_home]) == 4 - assert len(homeData.modules[homeData.default_home]) == 3 + assert len(homeData.modules[homeData.default_home]) == 5 expected = { "12:34:56:00:fa:d0": { @@ -25,7 +25,11 @@ def test_HomeData(homeData): "type": "NAPlug", "name": "Thermostat", "setup_date": 1494963356, - "modules_bridged": ["12:34:56:00:01:ae"], + "modules_bridged": [ + "12:34:56:00:01:ae", + "12:34:56:03:a0:ac", + "12:34:56:03:a5:54", + ], }, "12:34:56:00:01:ae": { "id": "12:34:56:00:01:ae", @@ -35,6 +39,22 @@ def test_HomeData(homeData): "room_id": "2746182631", "bridge": "12:34:56:00:fa:d0", }, + "12:34:56:03:a5:54": { + "id": "12:34:56:03:a5:54", + "type": "NRV", + "name": "Valve1", + "setup_date": 1554549767, + "room_id": "2833524037", + "bridge": "12:34:56:00:fa:d0", + }, + "12:34:56:03:a0:ac": { + "id": "12:34:56:03:a0:ac", + "type": "NRV", + "name": "Valve2", + "setup_date": 1554554444, + "room_id": "2940411577", + "bridge": "12:34:56:00:fa:d0", + }, "12:34:56:00:f1:62": { "id": "12:34:56:00:f1:62", "type": "NACamera", @@ -99,6 +119,13 @@ def test_HomeData_getSelectedschedule(homeData): (None, None, None, pytest.raises(smart_home.Thermostat.NoSchedule)), (None, None, "Default", does_not_raise()), (None, "591b54a2764ff4d50d8b5795", None, does_not_raise()), + (None, None, "Summer", pytest.raises(smart_home.Thermostat.NoSchedule)), + ( + None, + "123456789abcdefg12345678", + None, + pytest.raises(smart_home.Thermostat.NoSchedule), + ), ], ) def test_HomeData_switchHomeSchedule( @@ -118,7 +145,7 @@ def test_HomeData_switchHomeSchedule( def test_HomeStatus(homeStatus): - assert len(homeStatus.rooms) == 1 + assert len(homeStatus.rooms) == 3 assert homeStatus.default_room["id"] == "2746182631" expexted = { @@ -174,6 +201,20 @@ def test_HomeStatus_relayById(homeStatus): assert homeStatus.relayById("12:34:56:00:fa:d0") == expexted +def test_HomeStatus_valveById(homeStatus): + expexted = { + "id": "12:34:56:03:a5:54", + "reachable": True, + "type": "NRV", + "firmware_revision": 79, + "rf_strength": 51, + "battery_level": 3025, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "full", + } + assert homeStatus.valveById("12:34:56:03:a5:54") == expexted + + def test_HomeStatus_setPoint(homeStatus): assert homeStatus.setPoint("2746182631") == 12 From 823c93cc3818af670e94b7ea1014d1779b10f686 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 13 Jun 2019 23:55:55 +0200 Subject: [PATCH 102/333] Add another test --- fixtures/home_status_empty.json | 5 +++++ tests/test_smart_home_thermostat.py | 22 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 fixtures/home_status_empty.json diff --git a/fixtures/home_status_empty.json b/fixtures/home_status_empty.json new file mode 100644 index 000000000..62415ee2f --- /dev/null +++ b/fixtures/home_status_empty.json @@ -0,0 +1,5 @@ +{ + "status": "ok", + "time_server": 1559292039, + "body": {} +} \ No newline at end of file diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index cbd90bafe..0f6b45a94 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -160,6 +160,25 @@ def test_HomeStatus(homeStatus): assert homeStatus.default_room == expexted +def test_HomeStatus_error(auth, requests_mock): + with open("fixtures/home_status_empty.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Thermostat._GETHOMESTATUS_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with open("fixtures/home_data_simple.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Thermostat._GETHOMESDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with pytest.raises(smart_home.Thermostat.NoDevice): + assert smart_home.Thermostat.HomeStatus(auth) + + def test_HomeStatus_roomById(homeStatus): expexted = { "id": "2746182631", @@ -239,5 +258,6 @@ def test_HomeStatus_boilerStatus(homeStatus): assert homeStatus.boilerStatus() is False -def test_HomeStatus_thermostatType(homeStatus, homeData): +def test_HomeStatus_thermostatType(homeStatus): assert homeStatus.thermostatType("MYHOME", "2746182631") == "NATherm1" + assert homeStatus.thermostatType("MYHOME", "2833524037") == "NRV" From 0ea1c9258e7d9a404161688f0db6d6bf62316d4d Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 14 Jun 2019 00:17:45 +0200 Subject: [PATCH 103/333] Add weatherstation test fixture --- fixtures/weatherstation_data_simple.json | 203 +++++++++++++++++++++++ tests/conftest.py | 20 +++ tests/test_smart_home_weatherstation.py | 17 ++ 3 files changed, 240 insertions(+) create mode 100644 fixtures/weatherstation_data_simple.json create mode 100644 tests/test_smart_home_weatherstation.py diff --git a/fixtures/weatherstation_data_simple.json b/fixtures/weatherstation_data_simple.json new file mode 100644 index 000000000..19aa82b4b --- /dev/null +++ b/fixtures/weatherstation_data_simple.json @@ -0,0 +1,203 @@ +{ + "body": { + "devices": [ + { + "_id": "12:34:56:37:11:ca", + "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX", + "date_setup": 1544558432, + "last_setup": 1544558432, + "type": "NAMain", + "last_status_store": 1559413181, + "module_name": "NetatmoIndoor", + "firmware": 137, + "last_upgrade": 1544558433, + "wifi_status": 45, + "reachable": true, + "co2_calibrating": false, + "station_name": "MyStation", + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 664, + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [ + 52.516263, + 13.377726 + ] + }, + "dashboard_data": { + "time_utc": 1559413171, + "Temperature": 24.6, + "CO2": 749, + "Humidity": 36, + "Noise": 37, + "Pressure": 1017.3, + "AbsolutePressure": 939.7, + "min_temp": 23.4, + "max_temp": 24.6, + "date_min_temp": 1559371924, + "date_max_temp": 1559411964, + "temp_trend": "stable", + "pressure_trend": "down" + }, + "modules": [ + { + "_id": "12:34:56:36:fc:de", + "type": "NAModule1", + "module_name": "NetatmoOutdoor", + "data_type": [ + "Temperature", + "Humidity" + ], + "last_setup": 1544558433, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413157, + "Temperature": 28.6, + "Humidity": 24, + "min_temp": 16.9, + "max_temp": 30.3, + "date_min_temp": 1559365579, + "date_max_temp": 1559404698, + "temp_trend": "down" + }, + "firmware": 46, + "last_message": 1559413177, + "last_seen": 1559413157, + "rf_status": 65, + "battery_vp": 5738, + "battery_percent": 87 + }, + { + "_id": "12:34:56:07:bb:3e", + "type": "NAModule4", + "module_name": "Kitchen", + "data_type": [ + "Temperature", + "CO2", + "Humidity" + ], + "last_setup": 1548956696, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413125, + "Temperature": 28, + "CO2": 503, + "Humidity": 26, + "min_temp": 25, + "max_temp": 28, + "date_min_temp": 1559371577, + "date_max_temp": 1559412561, + "temp_trend": "up" + }, + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 73, + "battery_vp": 5687, + "battery_percent": 83 + }, + { + "_id": "12:34:56:07:bb:0e", + "type": "NAModule4", + "module_name": "Livingroom", + "data_type": [ + "Temperature", + "CO2", + "Humidity" + ], + "last_setup": 1548957209, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413093, + "Temperature": 26.4, + "CO2": 451, + "Humidity": 31, + "min_temp": 25.1, + "max_temp": 26.4, + "date_min_temp": 1559365290, + "date_max_temp": 1559413093, + "temp_trend": "stable" + }, + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413093, + "rf_status": 84, + "battery_vp": 5626, + "battery_percent": 79 + }, + { + "_id": "12:34:56:03:1b:e4", + "type": "NAModule2", + "module_name": "Garden", + "data_type": [ + "Wind" + ], + "last_setup": 1549193862, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "WindStrength": 4, + "WindAngle": 217, + "GustStrength": 9, + "GustAngle": 206, + "max_wind_str": 21, + "max_wind_angle": 217, + "date_max_wind_str": 1559386669 + }, + "firmware": 19, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 59, + "battery_vp": 5689, + "battery_percent": 85 + }, + { + "_id": "12:34:56:05:51:20", + "type": "NAModule3", + "module_name": "Yard", + "data_type": [ + "Rain" + ], + "last_setup": 1549194580, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "Rain": 0, + "sum_rain_24": 0, + "sum_rain_1": 0 + }, + "firmware": 8, + "last_message": 1559413177, + "last_seen": 1559413170, + "rf_status": 67, + "battery_vp": 5860, + "battery_percent": 93 + } + ] + } + ], + "user": { + "mail": "john@doe.com", + "administrative": { + "lang": "de-DE", + "reg_locale": "de-DE", + "country": "DE", + "unit": 0, + "windunit": 0, + "pressureunit": 0, + "feel_like_algo": 0 + } + } + }, + "status": "ok", + "time_exec": 0.91107702255249, + "time_server": 1559413602 +} \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index ee33d6b21..11c7a2c2d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,14 @@ import pyatmo import smart_home.Thermostat import smart_home.PublicData +import smart_home.WeatherStation + +from contextlib import contextmanager + + +@contextmanager +def does_not_raise(): + yield @pytest.fixture(scope="function") @@ -70,3 +78,15 @@ def publicData(auth, requests_mock): headers={"content-type": "application/json"}, ) return pyatmo.PublicData(auth) + + +@pytest.fixture(scope="function") +def weatherStationData(auth, requests_mock): + with open("fixtures/weatherstation_data_simple.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.WeatherStation._GETSTATIONDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + return pyatmo.WeatherStationData(auth) diff --git a/tests/test_smart_home_weatherstation.py b/tests/test_smart_home_weatherstation.py new file mode 100644 index 000000000..718b3d38f --- /dev/null +++ b/tests/test_smart_home_weatherstation.py @@ -0,0 +1,17 @@ +"""Define tests for WeatherStation module.""" +import json + +import pytest + +import smart_home.WeatherStation + +from contextlib import contextmanager + + +@contextmanager +def does_not_raise(): + yield + + +def test_WeatherStationData(weatherStationData): + assert weatherStationData.default_station == "MyStation" From 51db7d75dab7753b0ffdabc3eed68b2139b560a8 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 14 Jun 2019 08:03:20 +0200 Subject: [PATCH 104/333] Add fixtures --- fixtures/home_data_nohomename.json | 404 ++++++++++++++++++++++++ fixtures/weatherstation_data_empty.json | 203 ++++++++++++ tests/test_smart_home_thermostat.py | 19 +- tests/test_smart_home_weatherstation.py | 40 ++- 4 files changed, 648 insertions(+), 18 deletions(-) create mode 100644 fixtures/home_data_nohomename.json create mode 100644 fixtures/weatherstation_data_empty.json diff --git a/fixtures/home_data_nohomename.json b/fixtures/home_data_nohomename.json new file mode 100644 index 000000000..dc85d3c9b --- /dev/null +++ b/fixtures/home_data_nohomename.json @@ -0,0 +1,404 @@ +{ + "body": { + "homes": [ + { + "id": "91763b24c43d3e344f424e8b", + "altitude": 112, + "coordinates": [ + 52.516263, + 13.377726 + ], + "country": "DE", + "timezone": "Europe/Berlin", + "rooms": [ + { + "id": "2746182631", + "name": "Livingroom", + "type": "livingroom", + "module_ids": [ + "12:34:56:00:01:ae" + ] + }, + { + "id": "3688132631", + "name": "Hall", + "type": "custom", + "module_ids": [ + "12:34:56:00:f1:62" + ] + } + ], + "modules": [ + { + "id": "12:34:56:00:fa:d0", + "type": "NAPlug", + "name": "Thermostat", + "setup_date": 1494963356, + "modules_bridged": [ + "12:34:56:00:01:ae" + ] + }, + { + "id": "12:34:56:00:01:ae", + "type": "NATherm1", + "name": "Livingroom", + "setup_date": 1494963356, + "room_id": "2746182631", + "bridge": "12:34:56:00:fa:d0" + }, + { + "id": "12:34:56:00:f1:62", + "type": "NACamera", + "name": "Hall", + "setup_date": 1544828430, + "room_id": "3688132631" + } + ], + "therm_schedules": [ + { + "zones": [ + { + "type": 0, + "name": "Comfort", + "rooms_temp": [ + { + "temp": 21, + "room_id": "2746182631" + } + ], + "id": 0 + }, + { + "type": 1, + "name": "Night", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 1 + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 4 + } + ], + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 360 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 960 + }, + { + "zone_id": 1, + "m_offset": 1410 + }, + { + "zone_id": 0, + "m_offset": 1800 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2400 + }, + { + "zone_id": 1, + "m_offset": 2850 + }, + { + "zone_id": 0, + "m_offset": 3240 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3840 + }, + { + "zone_id": 1, + "m_offset": 4290 + }, + { + "zone_id": 0, + "m_offset": 4680 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5280 + }, + { + "zone_id": 1, + "m_offset": 5730 + }, + { + "zone_id": 0, + "m_offset": 6120 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6720 + }, + { + "zone_id": 1, + "m_offset": 7170 + }, + { + "zone_id": 0, + "m_offset": 7620 + }, + { + "zone_id": 1, + "m_offset": 8610 + }, + { + "zone_id": 0, + "m_offset": 9060 + }, + { + "zone_id": 1, + "m_offset": 10050 + } + ], + "hg_temp": 7, + "away_temp": 14, + "name": "Default", + "selected": true, + "id": "591b54a2764ff4d50d8b5795", + "type": "therm" + } + ], + "therm_setpoint_default_duration": 120, + "persons": [ + { + "id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "pseudo": "John Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7" + }, + { + "id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "pseudo": "Jane Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff3", + "pseudo": "Richard Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" + } + ], + "schedules": [ + { + "zones": [ + { + "type": 0, + "name": "Komfort", + "rooms_temp": [ + { + "temp": 21, + "room_id": "2746182631" + } + ], + "id": 0, + "rooms": [ + { + "id": "2746182631", + "therm_setpoint_temperature": 21 + } + ] + }, + { + "type": 1, + "name": "Nacht", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 1, + "rooms": [ + { + "id": "2746182631", + "therm_setpoint_temperature": 17 + } + ] + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 4, + "rooms": [ + { + "id": "2746182631", + "therm_setpoint_temperature": 17 + } + ] + } + ], + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 360 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 960 + }, + { + "zone_id": 1, + "m_offset": 1410 + }, + { + "zone_id": 0, + "m_offset": 1800 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2400 + }, + { + "zone_id": 1, + "m_offset": 2850 + }, + { + "zone_id": 0, + "m_offset": 3240 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3840 + }, + { + "zone_id": 1, + "m_offset": 4290 + }, + { + "zone_id": 0, + "m_offset": 4680 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5280 + }, + { + "zone_id": 1, + "m_offset": 5730 + }, + { + "zone_id": 0, + "m_offset": 6120 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6720 + }, + { + "zone_id": 1, + "m_offset": 7170 + }, + { + "zone_id": 0, + "m_offset": 7620 + }, + { + "zone_id": 1, + "m_offset": 8610 + }, + { + "zone_id": 0, + "m_offset": 9060 + }, + { + "zone_id": 1, + "m_offset": 10050 + } + ], + "hg_temp": 7, + "away_temp": 14, + "name": "Default", + "id": "591b54a2764ff4d50d8b5795", + "selected": true, + "type": "therm" + } + ], + "therm_mode": "schedule" + } + ], + "user": { + "email": "john@doe.com", + "language": "de-DE", + "locale": "de-DE", + "feel_like_algorithm": 0, + "unit_pressure": 0, + "unit_system": 0, + "unit_wind": 0, + "id": "91763b24c43d3e344f424e8b" + } + }, + "status": "ok", + "time_exec": 0.056135892868042, + "time_server": 1559171003 +} \ No newline at end of file diff --git a/fixtures/weatherstation_data_empty.json b/fixtures/weatherstation_data_empty.json new file mode 100644 index 000000000..19aa82b4b --- /dev/null +++ b/fixtures/weatherstation_data_empty.json @@ -0,0 +1,203 @@ +{ + "body": { + "devices": [ + { + "_id": "12:34:56:37:11:ca", + "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX", + "date_setup": 1544558432, + "last_setup": 1544558432, + "type": "NAMain", + "last_status_store": 1559413181, + "module_name": "NetatmoIndoor", + "firmware": 137, + "last_upgrade": 1544558433, + "wifi_status": 45, + "reachable": true, + "co2_calibrating": false, + "station_name": "MyStation", + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 664, + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [ + 52.516263, + 13.377726 + ] + }, + "dashboard_data": { + "time_utc": 1559413171, + "Temperature": 24.6, + "CO2": 749, + "Humidity": 36, + "Noise": 37, + "Pressure": 1017.3, + "AbsolutePressure": 939.7, + "min_temp": 23.4, + "max_temp": 24.6, + "date_min_temp": 1559371924, + "date_max_temp": 1559411964, + "temp_trend": "stable", + "pressure_trend": "down" + }, + "modules": [ + { + "_id": "12:34:56:36:fc:de", + "type": "NAModule1", + "module_name": "NetatmoOutdoor", + "data_type": [ + "Temperature", + "Humidity" + ], + "last_setup": 1544558433, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413157, + "Temperature": 28.6, + "Humidity": 24, + "min_temp": 16.9, + "max_temp": 30.3, + "date_min_temp": 1559365579, + "date_max_temp": 1559404698, + "temp_trend": "down" + }, + "firmware": 46, + "last_message": 1559413177, + "last_seen": 1559413157, + "rf_status": 65, + "battery_vp": 5738, + "battery_percent": 87 + }, + { + "_id": "12:34:56:07:bb:3e", + "type": "NAModule4", + "module_name": "Kitchen", + "data_type": [ + "Temperature", + "CO2", + "Humidity" + ], + "last_setup": 1548956696, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413125, + "Temperature": 28, + "CO2": 503, + "Humidity": 26, + "min_temp": 25, + "max_temp": 28, + "date_min_temp": 1559371577, + "date_max_temp": 1559412561, + "temp_trend": "up" + }, + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 73, + "battery_vp": 5687, + "battery_percent": 83 + }, + { + "_id": "12:34:56:07:bb:0e", + "type": "NAModule4", + "module_name": "Livingroom", + "data_type": [ + "Temperature", + "CO2", + "Humidity" + ], + "last_setup": 1548957209, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413093, + "Temperature": 26.4, + "CO2": 451, + "Humidity": 31, + "min_temp": 25.1, + "max_temp": 26.4, + "date_min_temp": 1559365290, + "date_max_temp": 1559413093, + "temp_trend": "stable" + }, + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413093, + "rf_status": 84, + "battery_vp": 5626, + "battery_percent": 79 + }, + { + "_id": "12:34:56:03:1b:e4", + "type": "NAModule2", + "module_name": "Garden", + "data_type": [ + "Wind" + ], + "last_setup": 1549193862, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "WindStrength": 4, + "WindAngle": 217, + "GustStrength": 9, + "GustAngle": 206, + "max_wind_str": 21, + "max_wind_angle": 217, + "date_max_wind_str": 1559386669 + }, + "firmware": 19, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 59, + "battery_vp": 5689, + "battery_percent": 85 + }, + { + "_id": "12:34:56:05:51:20", + "type": "NAModule3", + "module_name": "Yard", + "data_type": [ + "Rain" + ], + "last_setup": 1549194580, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "Rain": 0, + "sum_rain_24": 0, + "sum_rain_1": 0 + }, + "firmware": 8, + "last_message": 1559413177, + "last_seen": 1559413170, + "rf_status": 67, + "battery_vp": 5860, + "battery_percent": 93 + } + ] + } + ], + "user": { + "mail": "john@doe.com", + "administrative": { + "lang": "de-DE", + "reg_locale": "de-DE", + "country": "DE", + "unit": 0, + "windunit": 0, + "pressureunit": 0, + "feel_like_algo": 0 + } + } + }, + "status": "ok", + "time_exec": 0.91107702255249, + "time_server": 1559413602 +} \ No newline at end of file diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index 0f6b45a94..b2d9b973a 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -3,14 +3,9 @@ import pytest -import smart_home.Thermostat - -from contextlib import contextmanager +from .conftest import does_not_raise - -@contextmanager -def does_not_raise(): - yield +import smart_home.Thermostat def test_HomeData(homeData): @@ -66,13 +61,13 @@ def test_HomeData(homeData): assert homeData.modules[homeData.default_home] == expected -def test_HomeData_noData(auth, requests_mock): +def test_HomeData_no_data(auth, requests_mock): requests_mock.post(smart_home.Thermostat._GETHOMESDATA_REQ, text="None") - with pytest.raises(smart_home.PublicData.NoDevice): + with pytest.raises(smart_home.Thermostat.NoDevice): assert smart_home.Thermostat.HomeData(auth) -def test_HomeData_noBody(auth, requests_mock): +def test_HomeData_no_body(auth, requests_mock): with open("fixtures/home_data_empty.json") as f: json_fixture = json.load(f) requests_mock.post( @@ -80,11 +75,11 @@ def test_HomeData_noBody(auth, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(smart_home.PublicData.NoDevice): + with pytest.raises(smart_home.Thermostat.NoDevice): assert smart_home.Thermostat.HomeData(auth) -def test_HomeData_noHomeName(auth, requests_mock): +def test_HomeData_no_home_name(auth, requests_mock): with open("fixtures/home_data_nohomename.json") as f: json_fixture = json.load(f) requests_mock.post( diff --git a/tests/test_smart_home_weatherstation.py b/tests/test_smart_home_weatherstation.py index 718b3d38f..bc8d98dfe 100644 --- a/tests/test_smart_home_weatherstation.py +++ b/tests/test_smart_home_weatherstation.py @@ -3,15 +3,43 @@ import pytest +from .conftest import does_not_raise + import smart_home.WeatherStation -from contextlib import contextmanager + +def test_WeatherStationData(weatherStationData): + assert weatherStationData.default_station == "MyStation" -@contextmanager -def does_not_raise(): - yield +def test_WeatherStationData_no_response(auth, requests_mock): + requests_mock.post(smart_home.WeatherStation._GETSTATIONDATA_REQ, text="None") + with pytest.raises(smart_home.WeatherStation.NoDevice): + assert smart_home.WeatherStation.WeatherStationData(auth) + + +def test_WeatherStationData_no_body(auth, requests_mock): + with open("fixtures/status_ok.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.WeatherStation._GETSTATIONDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with pytest.raises(smart_home.WeatherStation.NoDevice): + assert smart_home.WeatherStation.WeatherStationData(auth) + + +def test_WeatherStationData_no_data(auth, requests_mock): + with open("fixtures/home_data_empty.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.WeatherStation._GETSTATIONDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with pytest.raises(smart_home.WeatherStation.NoDevice): + assert smart_home.WeatherStation.WeatherStationData(auth) + -def test_WeatherStationData(weatherStationData): - assert weatherStationData.default_station == "MyStation" From 690a474ec06c24331b575aabee4b47a20afe9863 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 14 Jun 2019 08:34:51 +0200 Subject: [PATCH 105/333] Remove unused fixture --- fixtures/weatherstation_data_empty.json | 203 ------------------------ tests/test_smart_home_weatherstation.py | 3 - 2 files changed, 206 deletions(-) delete mode 100644 fixtures/weatherstation_data_empty.json diff --git a/fixtures/weatherstation_data_empty.json b/fixtures/weatherstation_data_empty.json deleted file mode 100644 index 19aa82b4b..000000000 --- a/fixtures/weatherstation_data_empty.json +++ /dev/null @@ -1,203 +0,0 @@ -{ - "body": { - "devices": [ - { - "_id": "12:34:56:37:11:ca", - "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX", - "date_setup": 1544558432, - "last_setup": 1544558432, - "type": "NAMain", - "last_status_store": 1559413181, - "module_name": "NetatmoIndoor", - "firmware": 137, - "last_upgrade": 1544558433, - "wifi_status": 45, - "reachable": true, - "co2_calibrating": false, - "station_name": "MyStation", - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure" - ], - "place": { - "altitude": 664, - "city": "Frankfurt", - "country": "DE", - "timezone": "Europe/Berlin", - "location": [ - 52.516263, - 13.377726 - ] - }, - "dashboard_data": { - "time_utc": 1559413171, - "Temperature": 24.6, - "CO2": 749, - "Humidity": 36, - "Noise": 37, - "Pressure": 1017.3, - "AbsolutePressure": 939.7, - "min_temp": 23.4, - "max_temp": 24.6, - "date_min_temp": 1559371924, - "date_max_temp": 1559411964, - "temp_trend": "stable", - "pressure_trend": "down" - }, - "modules": [ - { - "_id": "12:34:56:36:fc:de", - "type": "NAModule1", - "module_name": "NetatmoOutdoor", - "data_type": [ - "Temperature", - "Humidity" - ], - "last_setup": 1544558433, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413157, - "Temperature": 28.6, - "Humidity": 24, - "min_temp": 16.9, - "max_temp": 30.3, - "date_min_temp": 1559365579, - "date_max_temp": 1559404698, - "temp_trend": "down" - }, - "firmware": 46, - "last_message": 1559413177, - "last_seen": 1559413157, - "rf_status": 65, - "battery_vp": 5738, - "battery_percent": 87 - }, - { - "_id": "12:34:56:07:bb:3e", - "type": "NAModule4", - "module_name": "Kitchen", - "data_type": [ - "Temperature", - "CO2", - "Humidity" - ], - "last_setup": 1548956696, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413125, - "Temperature": 28, - "CO2": 503, - "Humidity": 26, - "min_temp": 25, - "max_temp": 28, - "date_min_temp": 1559371577, - "date_max_temp": 1559412561, - "temp_trend": "up" - }, - "firmware": 44, - "last_message": 1559413177, - "last_seen": 1559413177, - "rf_status": 73, - "battery_vp": 5687, - "battery_percent": 83 - }, - { - "_id": "12:34:56:07:bb:0e", - "type": "NAModule4", - "module_name": "Livingroom", - "data_type": [ - "Temperature", - "CO2", - "Humidity" - ], - "last_setup": 1548957209, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413093, - "Temperature": 26.4, - "CO2": 451, - "Humidity": 31, - "min_temp": 25.1, - "max_temp": 26.4, - "date_min_temp": 1559365290, - "date_max_temp": 1559413093, - "temp_trend": "stable" - }, - "firmware": 44, - "last_message": 1559413177, - "last_seen": 1559413093, - "rf_status": 84, - "battery_vp": 5626, - "battery_percent": 79 - }, - { - "_id": "12:34:56:03:1b:e4", - "type": "NAModule2", - "module_name": "Garden", - "data_type": [ - "Wind" - ], - "last_setup": 1549193862, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413170, - "WindStrength": 4, - "WindAngle": 217, - "GustStrength": 9, - "GustAngle": 206, - "max_wind_str": 21, - "max_wind_angle": 217, - "date_max_wind_str": 1559386669 - }, - "firmware": 19, - "last_message": 1559413177, - "last_seen": 1559413177, - "rf_status": 59, - "battery_vp": 5689, - "battery_percent": 85 - }, - { - "_id": "12:34:56:05:51:20", - "type": "NAModule3", - "module_name": "Yard", - "data_type": [ - "Rain" - ], - "last_setup": 1549194580, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413170, - "Rain": 0, - "sum_rain_24": 0, - "sum_rain_1": 0 - }, - "firmware": 8, - "last_message": 1559413177, - "last_seen": 1559413170, - "rf_status": 67, - "battery_vp": 5860, - "battery_percent": 93 - } - ] - } - ], - "user": { - "mail": "john@doe.com", - "administrative": { - "lang": "de-DE", - "reg_locale": "de-DE", - "country": "DE", - "unit": 0, - "windunit": 0, - "pressureunit": 0, - "feel_like_algo": 0 - } - } - }, - "status": "ok", - "time_exec": 0.91107702255249, - "time_server": 1559413602 -} \ No newline at end of file diff --git a/tests/test_smart_home_weatherstation.py b/tests/test_smart_home_weatherstation.py index bc8d98dfe..159bf6f16 100644 --- a/tests/test_smart_home_weatherstation.py +++ b/tests/test_smart_home_weatherstation.py @@ -40,6 +40,3 @@ def test_WeatherStationData_no_data(auth, requests_mock): ) with pytest.raises(smart_home.WeatherStation.NoDevice): assert smart_home.WeatherStation.WeatherStationData(auth) - - - From e26141cd930be135c406133bef1905b32d3356a1 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 14 Jun 2019 13:20:55 +0200 Subject: [PATCH 106/333] Add weatherstation tests --- README.md | 19 +++ fixtures/home_coach_simple.json | 114 +++++++++++++ tests/test_smart_home_weatherstation.py | 211 ++++++++++++++++++++++++ 3 files changed, 344 insertions(+) create mode 100644 fixtures/home_coach_simple.json diff --git a/README.md b/README.md index e79e1e9e5..ca28b362a 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,22 @@ Once installed you can simple add `pyatmo` to your Python 3 scripts by including The module requires a valid user account and a registered application. See usage.md for further information. Be aware that the module may stop working if Netatmo decides to change their API. + + +### Testing ### + +To run the pytest testsuite you need to install the following dependencies: + + pip install pytest pytest-mock pytest-cov requests-mock freezegun + +To run the full suite simply type in + + pytest + +or + + python -m pytest + +To generate code coverage xml (e.g. for use in VSCode) run + + python -m pytest --cov-report xml:cov.xml --cov smart_home --cov-append tests/ \ No newline at end of file diff --git a/fixtures/home_coach_simple.json b/fixtures/home_coach_simple.json new file mode 100644 index 000000000..08c1c1e05 --- /dev/null +++ b/fixtures/home_coach_simple.json @@ -0,0 +1,114 @@ +{ + "body": { + "devices": [ + { + "_id": "12:34:56:26:69:0c", + "cipher_id": "enc:16:1UqwQlYV5AY2pfyEi5H47dmmFOOL3mCUo+KAkchL4A2CLI5u0e45Xr5jeAswO+XO", + "date_setup": 1544560184, + "last_setup": 1544560184, + "type": "NHC", + "last_status_store": 1558268332, + "firmware": 45, + "last_upgrade": 1544560186, + "wifi_status": 58, + "reachable": false, + "co2_calibrating": false, + "station_name": "Bedroom", + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure", + "health_idx" + ], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [ + 52.516263, + 13.377726 + ] + } + }, + { + "_id": "12:34:56:25:cf:a8", + "cipher_id": "enc:16:A+Jm0yFWBwUyKinFDutPZK7I2PuHN1fqaE9oB/KF+McbFs3oN9CKpR/dYbqL4om2", + "date_setup": 1544562192, + "last_setup": 1544562192, + "type": "NHC", + "last_status_store": 1559198922, + "firmware": 45, + "last_upgrade": 1544562194, + "wifi_status": 41, + "reachable": false, + "co2_calibrating": false, + "station_name": "Kitchen", + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure", + "health_idx" + ], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [ + 52.516263, + 13.377726 + ] + } + }, + { + "_id": "12:34:56:26:65:14", + "cipher_id": "enc:16:7kK6ZzG4L7NgfZZ6+dMvNxw4l6vXu+88SEJkCUklNdPa4KYIHmsfa1moOilEK61i", + "date_setup": 1544564061, + "last_setup": 1544564061, + "type": "NHC", + "last_status_store": 1559067159, + "firmware": 45, + "last_upgrade": 1544564302, + "wifi_status": 66, + "reachable": false, + "co2_calibrating": false, + "station_name": "Livingroom", + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure", + "health_idx" + ], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [ + 52.516263, + 13.377726 + ] + } + } + ], + "user": { + "mail": "john@doe.com", + "administrative": { + "lang": "de-DE", + "reg_locale": "de-DE", + "country": "DE", + "unit": 0, + "windunit": 0, + "pressureunit": 0, + "feel_like_algo": 0 + } + } + }, + "status": "ok", + "time_exec": 0.095954179763794, + "time_server": 1559463229 +} \ No newline at end of file diff --git a/tests/test_smart_home_weatherstation.py b/tests/test_smart_home_weatherstation.py index 159bf6f16..f9f8e6d12 100644 --- a/tests/test_smart_home_weatherstation.py +++ b/tests/test_smart_home_weatherstation.py @@ -3,6 +3,8 @@ import pytest +from freezegun import freeze_time + from .conftest import does_not_raise import smart_home.WeatherStation @@ -40,3 +42,212 @@ def test_WeatherStationData_no_data(auth, requests_mock): ) with pytest.raises(smart_home.WeatherStation.NoDevice): assert smart_home.WeatherStation.WeatherStationData(auth) + + +@pytest.mark.parametrize( + "station, expected", + [ + ( + None, + [ + "Garden", + "Kitchen", + "Livingroom", + "NetatmoIndoor", + "NetatmoOutdoor", + "Yard", + ], + ), + ( + "MyStation", + [ + "Garden", + "Kitchen", + "Livingroom", + "NetatmoIndoor", + "NetatmoOutdoor", + "Yard", + ], + ), + pytest.param( + "NoValidStation", + None, + marks=pytest.mark.skip("Invalid station names are not handled yet."), + ), + ], +) +def test_WeatherStationData_modulesNamesList(weatherStationData, station, expected): + assert sorted(weatherStationData.modulesNamesList(station)) == expected + + +def test_WeatherStationData_stationByName(weatherStationData): + result = weatherStationData.stationByName() + assert result["_id"] == "12:34:56:37:11:ca" + assert result["station_name"] == "MyStation" + assert result["module_name"] == "NetatmoIndoor" + assert result["type"] == "NAMain" + assert result["data_type"] == [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure", + ] + assert weatherStationData.stationByName("NoValidStation") is None + + +@pytest.mark.parametrize( + "module, station, expected", + [ + ("Kitchen", None, "12:34:56:07:bb:3e"), + ("Kitchen", "MyStation", "12:34:56:07:bb:3e"), + ("Kitchen", "NoValidStation", None), + ("NetatmoIndoor", None, "12:34:56:37:11:ca"), + ("NetatmoIndoor", "MyStation", "12:34:56:37:11:ca"), + ("", None, None), + ("", "", None), + (None, None, None), + ], +) +def test_WeatherStationData_moduleByName(weatherStationData, module, station, expected): + mod = weatherStationData.moduleByName(module, station) + if mod: + assert mod["_id"] == expected + else: + assert mod is expected + + +@pytest.mark.parametrize( + "mid, sid, expected", + [ + ("12:34:56:07:bb:3e", None, "12:34:56:07:bb:3e"), + ("12:34:56:07:bb:3e", "12:34:56:37:11:ca", "12:34:56:07:bb:3e"), + ("", None, None), + ("", "", None), + (None, None, None), + ], +) +def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): + mod = weatherStationData.moduleById(mid, sid) + if mod: + assert mod["_id"] == expected + else: + assert mod is expected + + +@pytest.mark.parametrize( + "module, expected", + [ + ( + "Kitchen", + [ + "battery_percent", + "battery_vp", + "co2", + "humidity", + "rf_status", + "temperature", + ], + ), + ( + "Garden", + [ + "battery_percent", + "battery_vp", + "gustangle", + "guststrength", + "rf_status", + "windangle", + "windstrength", + ], + ), + ( + "Yard", + [ + "Rain", + "battery_percent", + "battery_vp", + "rf_status", + "sum_rain_1", + "sum_rain_24", + ], + ), + ( + "NetatmoIndoor", + ["co2", "humidity", "noise", "pressure", "temperature", "wifi_status"], + ), + pytest.param( + "12:34:56:07:bb:3e", + None, + marks=pytest.mark.skip("Invalid module names are not handled yet."), + ), + pytest.param( + "", + None, + marks=pytest.mark.skip("Invalid module names are not handled yet."), + ), + pytest.param( + None, + None, + marks=pytest.mark.skip("Invalid module names are not handled yet."), + ), + ], +) +def test_WeatherStationData_monitoredConditions(weatherStationData, module, expected): + assert sorted(weatherStationData.monitoredConditions(module)) == expected + + +@freeze_time("2019-06-11") +@pytest.mark.parametrize( + "station, exclude, expected", + [ + ( + "MyStation", + None, + [ + "Garden", + "Kitchen", + "Livingroom", + "NetatmoIndoor", + "NetatmoOutdoor", + "Yard", + ], + ), + ( + "", + None, + [ + "Garden", + "Kitchen", + "Livingroom", + "NetatmoIndoor", + "NetatmoOutdoor", + "Yard", + ], + ), + ("NoValidStation", None, None), + ( + None, + 1000000, + [ + "Garden", + "Kitchen", + "Livingroom", + "NetatmoIndoor", + "NetatmoOutdoor", + "Yard", + ], + ), + ( + None, + 798103, + ["Garden", "Kitchen", "NetatmoIndoor", "NetatmoOutdoor", "Yard"], + ), + ], +) +def test_WeatherStationData_lastData(weatherStationData, station, exclude, expected): + mod = weatherStationData.lastData(station, exclude) + if mod: + assert sorted(mod) == expected + else: + assert mod is expected From 58b9e8d81426573acb6e0949b768262e1dd8e09e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 14 Jun 2019 23:49:05 +0200 Subject: [PATCH 107/333] Add tests for home coach devices --- tests/conftest.py | 14 ++++ tests/test_smart_home_homecoach.py | 45 +++++++++++++ tests/test_smart_home_weatherstation.py | 90 +++++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 tests/test_smart_home_homecoach.py diff --git a/tests/conftest.py b/tests/conftest.py index 11c7a2c2d..19638bd58 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ import smart_home.Thermostat import smart_home.PublicData import smart_home.WeatherStation +import smart_home.HomeCoach from contextlib import contextmanager @@ -90,3 +91,16 @@ def weatherStationData(auth, requests_mock): headers={"content-type": "application/json"}, ) return pyatmo.WeatherStationData(auth) + + +@pytest.fixture(scope="function") +def homeCoachData(auth, requests_mock): + with open("fixtures/home_coach_simple.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.HomeCoach._GETHOMECOACHDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + return pyatmo.HomeCoachData(auth) + diff --git a/tests/test_smart_home_homecoach.py b/tests/test_smart_home_homecoach.py new file mode 100644 index 000000000..af5698cd2 --- /dev/null +++ b/tests/test_smart_home_homecoach.py @@ -0,0 +1,45 @@ +"""Define tests for HomeCoach module.""" +import json + +import pytest + +from freezegun import freeze_time + +from .conftest import does_not_raise + +import smart_home.HomeCoach + + +def test_HomeCoachData(homeCoachData): + assert homeCoachData.default_station == "Bedroom" + + +@pytest.mark.parametrize( + "station, expected", + [ + ( + None, + [ + "Bedroom", + "Kitchen", + "Livingroom", + ], + ), + ( + "Bedroom", + [ + "Bedroom", + "Kitchen", + "Livingroom", + ], + ), + pytest.param( + "NoValidStation", + None, + marks=pytest.mark.skip("Invalid station names are not handled yet."), + ), + ], +) +def test_HomeCoachData_modulesNamesList(homeCoachData, station, expected): + assert sorted(homeCoachData.modulesNamesList(station)) == expected + diff --git a/tests/test_smart_home_weatherstation.py b/tests/test_smart_home_weatherstation.py index f9f8e6d12..b186063a9 100644 --- a/tests/test_smart_home_weatherstation.py +++ b/tests/test_smart_home_weatherstation.py @@ -251,3 +251,93 @@ def test_WeatherStationData_lastData(weatherStationData, station, exclude, expec assert sorted(mod) == expected else: assert mod is expected + + +@freeze_time("2019-06-11") +@pytest.mark.parametrize( + "station, delay, expected", + [ + ( + "MyStation", + 3600, + [ + "Garden", + "Kitchen", + "Livingroom", + "NetatmoIndoor", + "NetatmoOutdoor", + "Yard", + ], + ), + ( + None, + 3600, + [ + "Garden", + "Kitchen", + "Livingroom", + "NetatmoIndoor", + "NetatmoOutdoor", + "Yard", + ], + ), + ( + "", + 3600, + [ + "Garden", + "Kitchen", + "Livingroom", + "NetatmoIndoor", + "NetatmoOutdoor", + "Yard", + ], + ), + pytest.param( + "NoValidStation", + 3600, + None, + marks=pytest.mark.skip("Invalid station name not handled yet"), + ), + ], +) +def test_WeatherStationData_checkNotUpdated( + weatherStationData, station, delay, expected +): + mod = weatherStationData.checkNotUpdated(station, delay) + assert sorted(mod) == expected + + +@freeze_time("2019-06-11") +@pytest.mark.parametrize( + "station, delay, expected", + [ + ( + "MyStation", + 798500, + [ + "Garden", + "Kitchen", + "Livingroom", + "NetatmoIndoor", + "NetatmoOutdoor", + "Yard", + ], + ), + ( + None, + 798500, + [ + "Garden", + "Kitchen", + "Livingroom", + "NetatmoIndoor", + "NetatmoOutdoor", + "Yard", + ], + ), + ], +) +def test_WeatherStationData_checkUpdated(weatherStationData, station, delay, expected): + mod = weatherStationData.checkUpdated(station, delay) + assert sorted(mod) == expected From 85efa7baab95f261511b8e6686b474500e17a5cc Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 17 Jun 2019 15:45:32 +0200 Subject: [PATCH 108/333] Add getmeasure test --- fixtures/weatherstation_measure.json | 46 +++++++++++++++++++++++++ tests/test_smart_home_homecoach.py | 30 +++------------- tests/test_smart_home_weatherstation.py | 32 ++++++++++++++--- 3 files changed, 78 insertions(+), 30 deletions(-) create mode 100644 fixtures/weatherstation_measure.json diff --git a/fixtures/weatherstation_measure.json b/fixtures/weatherstation_measure.json new file mode 100644 index 000000000..1b7724ec6 --- /dev/null +++ b/fixtures/weatherstation_measure.json @@ -0,0 +1,46 @@ +{ + "body": { + "1544558433": [ + 28.1 + ], + "1544558449": [ + 28.4 + ], + "1544558504": [ + 27 + ], + "1544558807": [ + 24 + ], + "1544559062": [ + 23.8 + ], + "1544559211": [ + 26.1 + ], + "1544559308": [ + 24.9 + ], + "1544559415": [ + 24.6 + ], + "1544559576": [ + 24.2 + ], + "1544559974": [ + 26.9 + ], + "1544560021": [ + 27.1 + ], + "1544560058": [ + 27.4 + ], + "1544560361": [ + 26 + ] + }, + "status": "ok", + "time_exec": 0.33915495872498, + "time_server": 1560590041 +} \ No newline at end of file diff --git a/tests/test_smart_home_homecoach.py b/tests/test_smart_home_homecoach.py index af5698cd2..7166e9c94 100644 --- a/tests/test_smart_home_homecoach.py +++ b/tests/test_smart_home_homecoach.py @@ -1,14 +1,6 @@ """Define tests for HomeCoach module.""" -import json - import pytest -from freezegun import freeze_time - -from .conftest import does_not_raise - -import smart_home.HomeCoach - def test_HomeCoachData(homeCoachData): assert homeCoachData.default_station == "Bedroom" @@ -17,26 +9,14 @@ def test_HomeCoachData(homeCoachData): @pytest.mark.parametrize( "station, expected", [ - ( - None, - [ - "Bedroom", - "Kitchen", - "Livingroom", - ], - ), - ( - "Bedroom", - [ - "Bedroom", - "Kitchen", - "Livingroom", - ], - ), + (None, ["Bedroom", "Kitchen", "Livingroom"]), + ("Bedroom", ["Bedroom", "Kitchen", "Livingroom"]), pytest.param( "NoValidStation", None, - marks=pytest.mark.skip("Invalid station names are not handled yet."), + marks=pytest.mark.xfail( + reason="Invalid station names are not handled yet." + ), ), ], ) diff --git a/tests/test_smart_home_weatherstation.py b/tests/test_smart_home_weatherstation.py index b186063a9..f01605c31 100644 --- a/tests/test_smart_home_weatherstation.py +++ b/tests/test_smart_home_weatherstation.py @@ -72,7 +72,9 @@ def test_WeatherStationData_no_data(auth, requests_mock): pytest.param( "NoValidStation", None, - marks=pytest.mark.skip("Invalid station names are not handled yet."), + marks=pytest.mark.xfail( + reason="Invalid station names are not handled yet." + ), ), ], ) @@ -179,17 +181,17 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): pytest.param( "12:34:56:07:bb:3e", None, - marks=pytest.mark.skip("Invalid module names are not handled yet."), + marks=pytest.mark.xfail(reason="Invalid module names are not handled yet."), ), pytest.param( "", None, - marks=pytest.mark.skip("Invalid module names are not handled yet."), + marks=pytest.mark.xfail(reason="Invalid module names are not handled yet."), ), pytest.param( None, None, - marks=pytest.mark.skip("Invalid module names are not handled yet."), + marks=pytest.mark.xfail(reason="Invalid module names are not handled yet."), ), ], ) @@ -297,7 +299,7 @@ def test_WeatherStationData_lastData(weatherStationData, station, exclude, expec "NoValidStation", 3600, None, - marks=pytest.mark.skip("Invalid station name not handled yet"), + marks=pytest.mark.xfail(reason="Invalid station name not handled yet"), ), ], ) @@ -341,3 +343,23 @@ def test_WeatherStationData_checkNotUpdated( def test_WeatherStationData_checkUpdated(weatherStationData, station, delay, expected): mod = weatherStationData.checkUpdated(station, delay) assert sorted(mod) == expected + + +@freeze_time("2019-06-11") +@pytest.mark.parametrize( + "device_id, scale, mtype, expected", [("MyStation", "scale", "type", [28.1])] +) +def test_WeatherStationData_getMeasure( + weatherStationData, requests_mock, device_id, scale, mtype, expected +): + with open("fixtures/weatherstation_measure.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.WeatherStation._GETMEASURE_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + assert ( + weatherStationData.getMeasure(device_id, scale, mtype)["body"]["1544558433"] + == expected + ) From 04285e1bae7eafb22df825a8ce2a478a3fcb45d2 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 17 Jun 2019 15:46:27 +0200 Subject: [PATCH 109/333] Add tests for camera module --- fixtures/camera_home_data.json | 184 ++++++++++++++++++++++++ fixtures/camera_ping.json | 4 + tests/conftest.py | 11 ++ tests/test_smart_home_camera.py | 240 ++++++++++++++++++++++++++++++++ 4 files changed, 439 insertions(+) create mode 100644 fixtures/camera_home_data.json create mode 100644 fixtures/camera_ping.json create mode 100644 tests/test_smart_home_camera.py diff --git a/fixtures/camera_home_data.json b/fixtures/camera_home_data.json new file mode 100644 index 000000000..96f703a47 --- /dev/null +++ b/fixtures/camera_home_data.json @@ -0,0 +1,184 @@ +{ + "body": { + "homes": [ + { + "id": "91763b24c43d3e344f424e8b", + "name": "MYHOME", + "persons": [ + { + "id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "last_seen": 1557071156, + "out_of_sight": true, + "face": { + "id": "d74fad765b9100ef480720a9", + "version": 1, + "key": "a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7" + }, + "pseudo": "John Doe" + }, + { + "id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "last_seen": 1560600726, + "out_of_sight": true, + "face": { + "id": "d74fad765b9100ef480720a9", + "version": 3, + "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" + }, + "pseudo": "Jane Doe" + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff3", + "last_seen": 1560626666, + "out_of_sight": false, + "face": { + "id": "d74fad765b9100ef480720a9", + "version": 1, + "key": "a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" + }, + "pseudo": "Richard Doe" + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff4", + "last_seen": 1560621666, + "out_of_sight": true, + "face": { + "id": "d0ef44fad765b980720710a9", + "version": 1, + "key": "ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d0ef44fad765b980720710a9ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928" + } + } + ], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin" + }, + "cameras": [ + { + "id": "12:34:56:00:f1:62", + "type": "NACamera", + "status": "on", + "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTg,,", + "is_local": true, + "sd_status": "on", + "alim_status": "on", + "name": "Hall", + "use_pin_code": false, + "last_setup": 1544828430 + } + ], + "smokedetectors": [], + "events": [ + { + "id": "a1b2c3d4e5f6abcdef123456", + "type": "person", + "time": 1560604700, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "video_status": "deleted", + "is_arrival": false, + "message": "John Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef123457", + "type": "person_away", + "time": 1560602400, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "message": "John Doe hat das Haus verlassen", + "sub_message": "John Doe gilt als abwesend, da das mit diesem Profil verbundene Telefon den Bereich des Hauses verlassen hat." + }, + { + "id": "a1b2c3d4e5f6abcdef123458", + "type": "person", + "time": 1560601200, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "video_status": "deleted", + "is_arrival": false, + "message": "John Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef123459", + "type": "person", + "time": 1560600100, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "snapshot": { + "id": "d74fad765b9100ef480720a9", + "version": 1, + "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" + }, + "video_id": "12345678-36bc-4b9a-9762-5194e707ed51", + "video_status": "available", + "is_arrival": false, + "message": "Jane Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef12345a", + "type": "person", + "time": 1560603600, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827375-7e04-5298-83ae-a0cb8372dff3", + "snapshot": { + "id": "532dde8d17554c022ab071b8", + "version": 1, + "key": "9fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28", + "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b89fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28" + }, + "video_id": "12345678-1234-46cb-ad8f-23d893874099", + "video_status": "available", + "is_arrival": false, + "message": "Bewegung erkannt" + }, + { + "id": "a1b2c3d4e5f6abcdef12345b", + "type": "movement", + "time": 1560506200, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "category": "human", + "snapshot": { + "id": "532dde8d17554c022ab071b9", + "version": 1, + "key": "8fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28", + "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b98fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28" + }, + "vignette": { + "id": "5dc021b5dea854bd2321707a", + "version": 1, + "key": "58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944", + "url": "https://netatmocameraimage.blob.core.windows.net/production/5dc021b5dea854bd2321707a58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944" + }, + "video_id": "12345678-1234-46cb-ad8f-23d89387409a", + "video_status": "available", + "message": "Bewegung erkannt" + } + ] + } + ], + "user": { + "reg_locale": "de-DE", + "lang": "de-DE", + "country": "DE", + "mail": "john@doe.com" + }, + "global_info": { + "show_tags": true + } + }, + "status": "ok", + "time_exec": 0.03621506690979, + "time_server": 1560626960 +} \ No newline at end of file diff --git a/fixtures/camera_ping.json b/fixtures/camera_ping.json new file mode 100644 index 000000000..784975de5 --- /dev/null +++ b/fixtures/camera_ping.json @@ -0,0 +1,4 @@ +{ + "local_url": "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d", + "product_name": "Welcome Netatmo" +} \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 19638bd58..434f8cbe9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -104,3 +104,14 @@ def homeCoachData(auth, requests_mock): ) return pyatmo.HomeCoachData(auth) + +@pytest.fixture(scope="function") +def cameraHomeData(auth, requests_mock): + with open("fixtures/camera_home_data.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Camera._GETHOMEDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + return pyatmo.CameraData(auth) diff --git a/tests/test_smart_home_camera.py b/tests/test_smart_home_camera.py new file mode 100644 index 000000000..4b8d19978 --- /dev/null +++ b/tests/test_smart_home_camera.py @@ -0,0 +1,240 @@ +"""Define tests for Camera module.""" +import json + +import pytest + +from freezegun import freeze_time + +from .conftest import does_not_raise + +import smart_home.Camera + + +def test_CameraData(cameraHomeData): + assert cameraHomeData.default_home == "MYHOME" + assert cameraHomeData.default_camera["id"] == "12:34:56:00:f1:62" + assert cameraHomeData.default_camera["name"] == "Hall" + + +@pytest.mark.parametrize( + "hid, expected", + [ + ("91763b24c43d3e344f424e8b", "MYHOME"), + pytest.param( + None, + None, + marks=pytest.mark.xfail(reason="Invalid home id not handled yet"), + ), + ], +) +def test_CameraData_homeById(cameraHomeData, hid, expected): + assert cameraHomeData.homeById(hid)["name"] == expected + + +@pytest.mark.parametrize( + "name, expected", + [ + ("MYHOME", "91763b24c43d3e344f424e8b"), + (None, "91763b24c43d3e344f424e8b"), + ("", "91763b24c43d3e344f424e8b"), + pytest.param( + "InvalidName", + None, + marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), + ), + ], +) +def test_CameraData_homeByName(cameraHomeData, name, expected): + assert cameraHomeData.homeByName(name)["id"] == expected + + +@pytest.mark.parametrize( + "cid, expected", [("12:34:56:00:f1:62", "Hall"), ("None", None), (None, None)] +) +def test_CameraData_cameraById(cameraHomeData, cid, expected): + camera = cameraHomeData.cameraById(cid) + if camera: + assert camera["name"] == expected + else: + assert camera is expected + + +@pytest.mark.parametrize( + "name, home, expected", + [ + ("Hall", None, "12:34:56:00:f1:62"), + (None, None, "12:34:56:00:f1:62"), + ("", None, "12:34:56:00:f1:62"), + ("Hall", "MYHOME", "12:34:56:00:f1:62"), + (None, "MYHOME", "12:34:56:00:f1:62"), + ("", "MYHOME", "12:34:56:00:f1:62"), + pytest.param( + "InvalidName", + None, + None, + marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), + ), + pytest.param( + None, + "InvalidName", + None, + marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), + ), + ], +) +def test_CameraData_cameraByName(cameraHomeData, name, home, expected): + assert cameraHomeData.cameraByName(name, home)["id"] == expected + + +@pytest.mark.parametrize( + "camera, home, cid, expected", + [ + (None, None, None, "NACamera"), + ("Hall", None, None, "NACamera"), + ("Hall", "MYHOME", None, "NACamera"), + (None, "MYHOME", None, "NACamera"), + (None, "MYHOME", "12:34:56:00:f1:62", "NACamera"), + (None, None, "12:34:56:00:f1:62", "NACamera"), + ("InvalidName", None, None, None), + pytest.param( + None, + "InvalidName", + None, + "NACamera", + marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), + ), + ], +) +def test_CameraData_cameraType(cameraHomeData, camera, home, cid, expected): + assert cameraHomeData.cameraType(camera, home, cid) == expected + + +def test_CameraData_cameraUrls(cameraHomeData, requests_mock): + vpn_url = ( + "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/" + "6d278460699e56180d47ab47169efb31/" + "MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTg,," + ) + local_url = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" + with open("fixtures/camera_ping.json") as f: + json_fixture = json.load(f) + requests_mock.post( + vpn_url + "/command/ping", + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with open("fixtures/camera_ping.json") as f: + json_fixture = json.load(f) + requests_mock.post( + local_url + "/command/ping", + json=json_fixture, + headers={"content-type": "application/json"}, + ) + assert cameraHomeData.cameraUrls() == (vpn_url, local_url) + + +@pytest.mark.parametrize( + "home, expected", + [ + (None, ["Richard Doe"]), + ("MYHOME", ["Richard Doe"]), + pytest.param( + "InvalidHome", + None, + marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), + ), + ], +) +def test_CameraData_personsAtHome(cameraHomeData, home, expected): + assert cameraHomeData.personsAtHome(home) == expected + + +@freeze_time("2019-06-16") +@pytest.mark.parametrize( + "name, exclude, expected", + [ + ("John Doe", None, True), + ("Richard Doe", None, False), + ("Unknown", None, False), + ("John Doe", 1, False), + ("John Doe", 50000, True), + ("Jack Doe", None, False), + ], +) +def test_CameraData_personSeenByCamera(cameraHomeData, name, exclude, expected): + assert cameraHomeData.personSeenByCamera(name, exclude=exclude) is expected + + +def test_CameraData__knownPersons(cameraHomeData): + knownPersons = cameraHomeData._knownPersons() + assert len(knownPersons) == 3 + assert knownPersons["91827374-7e04-5298-83ad-a0cb8372dff1"]["pseudo"] == "John Doe" + + +def test_CameraData_knownPersonsNames(cameraHomeData): + assert sorted(cameraHomeData.knownPersonsNames()) == [ + "Jane Doe", + "John Doe", + "Richard Doe", + ] + + +@freeze_time("2019-06-16") +@pytest.mark.parametrize( + "home, camera, exclude, expected", + [ + (None, None, None, True), + (None, None, 5, False), + (None, "InvalidCamera", None, False), + pytest.param( + "InvalidHome", + None, + None, + True, + marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), + ), + ], +) +def test_CameraData_someoneKnownSeen(cameraHomeData, home, camera, exclude, expected): + assert cameraHomeData.someoneKnownSeen(home, camera, exclude) == expected + + +@freeze_time("2019-06-16") +@pytest.mark.parametrize( + "home, camera, exclude, expected", + [ + (None, None, None, False), + (None, None, 100, False), + (None, "InvalidCamera", None, False), + pytest.param( + "InvalidHome", + None, + None, + False, + marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), + ), + ], +) +def test_CameraData_someoneUnknownSeen(cameraHomeData, home, camera, exclude, expected): + assert cameraHomeData.someoneUnknownSeen(home, camera, exclude) == expected + + +@freeze_time("2019-06-16") +@pytest.mark.parametrize( + "home, camera, exclude, expected", + [ + (None, None, None, False), + (None, None, 140000, True), + (None, None, 130000, False), + (None, "InvalidCamera", None, False), + pytest.param( + "InvalidHome", + None, + None, + False, + marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), + ), + ], +) +def test_CameraData_motionDetected(cameraHomeData, home, camera, exclude, expected): + assert cameraHomeData.motionDetected(home, camera, exclude) == expected From 97fa678ce43f6debc0693858b32d8bc00aaa02ee Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 18 Jun 2019 16:20:41 +0200 Subject: [PATCH 110/333] Add more tests (#65) --- fixtures/home_status_error_invalid_id.json | 6 + .../home_status_error_missing_home_id.json | 6 + .../home_status_error_missing_parameters.json | 6 + .../home_status_error_mode_is_missing.json | 6 + ...home_status_error_mode_not_authorized.json | 6 + tests/test_smart_home_thermostat.py | 133 ++++++++++++++++++ 6 files changed, 163 insertions(+) create mode 100644 fixtures/home_status_error_invalid_id.json create mode 100644 fixtures/home_status_error_missing_home_id.json create mode 100644 fixtures/home_status_error_missing_parameters.json create mode 100644 fixtures/home_status_error_mode_is_missing.json create mode 100644 fixtures/home_status_error_mode_not_authorized.json diff --git a/fixtures/home_status_error_invalid_id.json b/fixtures/home_status_error_invalid_id.json new file mode 100644 index 000000000..c8868a91f --- /dev/null +++ b/fixtures/home_status_error_invalid_id.json @@ -0,0 +1,6 @@ +{ + "error": { + "code": 21, + "message": "Invalid id" + } +} \ No newline at end of file diff --git a/fixtures/home_status_error_missing_home_id.json b/fixtures/home_status_error_missing_home_id.json new file mode 100644 index 000000000..1a73477c4 --- /dev/null +++ b/fixtures/home_status_error_missing_home_id.json @@ -0,0 +1,6 @@ +{ + "error": { + "code": 10, + "message": "Missing home_id" + } +} \ No newline at end of file diff --git a/fixtures/home_status_error_missing_parameters.json b/fixtures/home_status_error_missing_parameters.json new file mode 100644 index 000000000..ee7c53d45 --- /dev/null +++ b/fixtures/home_status_error_missing_parameters.json @@ -0,0 +1,6 @@ +{ + "error": { + "code": 10, + "message": "Missing parameters" + } +} \ No newline at end of file diff --git a/fixtures/home_status_error_mode_is_missing.json b/fixtures/home_status_error_mode_is_missing.json new file mode 100644 index 000000000..14cf378dd --- /dev/null +++ b/fixtures/home_status_error_mode_is_missing.json @@ -0,0 +1,6 @@ +{ + "error": { + "code": 10, + "message": "mode is missing" + } +} \ No newline at end of file diff --git a/fixtures/home_status_error_mode_not_authorized.json b/fixtures/home_status_error_mode_not_authorized.json new file mode 100644 index 000000000..62764ee69 --- /dev/null +++ b/fixtures/home_status_error_mode_not_authorized.json @@ -0,0 +1,6 @@ +{ + "error": { + "code": 21, + "message": "mode not authorized" + } +} \ No newline at end of file diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index b2d9b973a..03f36fba0 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -1,5 +1,6 @@ """Define tests for Thermostat module.""" import json +import logging import pytest @@ -256,3 +257,135 @@ def test_HomeStatus_boilerStatus(homeStatus): def test_HomeStatus_thermostatType(homeStatus): assert homeStatus.thermostatType("MYHOME", "2746182631") == "NATherm1" assert homeStatus.thermostatType("MYHOME", "2833524037") == "NRV" + + +@pytest.mark.parametrize( + "home_id, mode, json_fixture, expected", + [ + (None, None, "home_status_error_mode_is_missing.json", "mode is missing"), + ( + "91763b24c43d3e344f424e8b", + None, + "home_status_error_mode_is_missing.json", + "mode is missing", + ), + ("invalidID", "away", "home_status_error_invalid_id.json", "Invalid id"), + ("91763b24c43d3e344f424e8b", "away", "status_ok.json", "ok"), + ], +) +def test_HomeData_setThermmode( + homeStatus, requests_mock, caplog, home_id, mode, json_fixture, expected +): + with open("fixtures/%s" % json_fixture) as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Thermostat._SETTHERMMODE_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with caplog.at_level(logging.DEBUG): + homeStatus.setThermmode(home_id=home_id, mode=mode) + assert expected in caplog.text + + +@pytest.mark.parametrize( + "home_id, room_id, mode, temp, json_fixture, expected", + [ + ("91763b24c43d3e344f424e8b", "2746182631", "home", 14, "status_ok.json", "ok"), + ( + "91763b24c43d3e344f424e8b", + "2746182631", + "home", + None, + "status_ok.json", + "ok", + ), + ], +) +def test_HomeData_setroomThermpoint( + homeStatus, + requests_mock, + caplog, + home_id, + room_id, + mode, + temp, + json_fixture, + expected, +): + with open("fixtures/%s" % json_fixture) as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Thermostat._SETROOMTHERMPOINT_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + assert ( + homeStatus.setroomThermpoint( + home_id=home_id, room_id=room_id, mode=mode, temp=temp + )["status"] + == expected + ) + + +@pytest.mark.parametrize( + "home_id, room_id, mode, temp, json_fixture, expected", + [ + ( + None, + None, + None, + None, + "home_status_error_missing_home_id.json", + "Missing home_id", + ), + ( + None, + None, + "home", + None, + "home_status_error_missing_home_id.json", + "Missing home_id", + ), + ( + "91763b24c43d3e344f424e8b", + None, + "home", + None, + "home_status_error_missing_parameters.json", + "Missing parameters", + ), + ( + "91763b24c43d3e344f424e8b", + "2746182631", + "home", + None, + "home_status_error_missing_parameters.json", + "Missing parameters", + ), + ], +) +def test_HomeData_setroomThermpoint_error( + homeStatus, + requests_mock, + caplog, + home_id, + room_id, + mode, + temp, + json_fixture, + expected, +): + with open("fixtures/%s" % json_fixture) as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Thermostat._SETROOMTHERMPOINT_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + assert ( + homeStatus.setroomThermpoint( + home_id=home_id, room_id=room_id, mode=mode, temp=temp + )["error"]["message"] + == expected + ) From b4a2ebe36a41e6ce5902826232e73d1f4129e6de Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sun, 23 Jun 2019 21:25:36 +0200 Subject: [PATCH 111/333] Refactor code (#66) * Do reverse while sorting * Use enumerate * Use enumerate * Use enumerate * Remove enumerate * Replace dict() * Update pep8 and flake8 ignores * Remove deprecated stuff * Don't raise exception when both data and error is in data * Add test for data and error in response * Fix missing camera url --- pyatmo.py | 10 ++- setup.cfg | 4 +- smart_home/Camera.py | 74 +++++++++----------- smart_home/Thermostat.py | 103 +++++++++++++--------------- smart_home/WeatherStation.py | 14 ++-- smart_home/__init__.py | 2 +- tests/test_smart_home_camera.py | 12 ++++ tests/test_smart_home_thermostat.py | 31 +++++++++ 8 files changed, 141 insertions(+), 109 deletions(-) diff --git a/pyatmo.py b/pyatmo.py index d86b094a2..449c20864 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -132,24 +132,22 @@ def accessToken(self): username=USERNAME, password=PASSWORD, scope="read_station read_camera access_camera read_thermostat write_thermostat read_presence access_presence", - ) # Test authentication method + ) try: - devList = DeviceList(authorization) # Test DEVICELIST + ws = WeatherStationData(authorization) except NoDevice: if stdout.isatty(): print("pyatmo.py : warning, no weather station available for testing") - else: - devList.MinMaxTH() # Test GETMEASUR try: - Camera = CameraData(authorization) + cam = CameraData(authorization) except NoDevice: if stdout.isatty(): print("pyatmo.py : warning, no camera available for testing") try: - Thermostat = ThermostatData(authorization) + hd = HomeData(authorization) except NoDevice: if stdout.isatty(): print("pyatmo.py : warning, no thermostat available for testing") diff --git a/setup.cfg b/setup.cfg index 8b5c68bad..581ae06d4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,6 +3,8 @@ description-file = README.md [flake8] max-line-length = 88 +ignore = W503, E501 [pep8] -max-line-length = 88 \ No newline at end of file +max-line-length = 88 +ignore = W503, E501 \ No newline at end of file diff --git a/smart_home/Camera.py b/smart_home/Camera.py index fabcde3b2..c489c0c55 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -33,44 +33,44 @@ def __init__(self, authData, size=15): self.homes = {d["id"]: d for d in self.rawData} if not self.homes: raise NoDevice("No camera available") - self.persons = dict() - self.events = dict() - self.outdoor_events = dict() - self.cameras = dict() - self.modules = dict() - self.lastEvent = dict() - self.outdoor_lastEvent = dict() - self.types = dict() + self.persons = {} + self.events = {} + self.outdoor_events = {} + self.cameras = {} + self.modules = {} + self.lastEvent = {} + self.outdoor_lastEvent = {} + self.types = {} self.default_home = None self.default_camera = None - for i in range(len(self.rawData)): - nameHome = self.rawData[i].get("name") + for item in self.rawData: + nameHome = item.get("name") if not nameHome: - raise NoDevice('No key ["name"] in %s' % self.rawData[i].keys()) + raise NoDevice('No key ["name"] in %s' % item.keys()) if nameHome not in self.cameras: - self.cameras[nameHome] = dict() + self.cameras[nameHome] = {} if nameHome not in self.types: - self.types[nameHome] = dict() - for p in self.rawData[i]["persons"]: + self.types[nameHome] = {} + for p in item["persons"]: self.persons[p["id"]] = p - if "events" in self.rawData[i]: - self.default_home = self.rawData[i]["name"] - for e in self.rawData[i]["events"]: + if "events" in item: + self.default_home = item["name"] + for e in item["events"]: if e["type"] == "outdoor": if e["camera_id"] not in self.outdoor_events: - self.outdoor_events[e["camera_id"]] = dict() + self.outdoor_events[e["camera_id"]] = {} self.outdoor_events[e["camera_id"]][e["time"]] = e elif e["type"] != "outdoor": if e["camera_id"] not in self.events: - self.events[e["camera_id"]] = dict() + self.events[e["camera_id"]] = {} self.events[e["camera_id"]][e["time"]] = e - for c in self.rawData[i]["cameras"]: + for c in item["cameras"]: self.cameras[nameHome][c["id"]] = c if c["type"] == "NACamera" and "modules" in c: for m in c["modules"]: self.modules[m["id"]] = m self.modules[m["id"]]["cam_id"] = c["id"] - for t in self.rawData[i]["cameras"]: + for t in item["cameras"]: self.types[nameHome][t["type"]] = t for camera in self.events: self.lastEvent[camera] = self.events[camera][ @@ -167,11 +167,11 @@ def cameraUrls(self, camera=None, home=None, cid=None): else: camera_data = self.cameraByName(camera=camera, home=home) if camera_data: - vpn_url = camera_data["vpn_url"] - if camera_data["is_local"]: + vpn_url = camera_data.get("vpn_url") + if camera_data.get("is_local"): try: resp = postRequest( - "{0}/command/ping".format(camera_data["vpn_url"]), dict() + "{0}/command/ping".format(vpn_url), {} ) temp_local_url = resp["local_url"] except URLError: @@ -179,7 +179,7 @@ def cameraUrls(self, camera=None, home=None, cid=None): try: resp = postRequest( - "{0}/command/ping".format(temp_local_url), dict() + "{0}/command/ping".format(temp_local_url), {} ) if temp_local_url == resp["local_url"]: local_url = temp_local_url @@ -238,7 +238,7 @@ def updateEvent(self, event=None, home=None, cameratype=None): if not event: # If not event is provided we need to retrieve the oldest of # the last event seen by each camera - listEvent = dict() + listEvent = {} for cam_id in self.lastEvent: listEvent[self.lastEvent[cam_id]["time"]] = self.lastEvent[cam_id] event = listEvent[sorted(listEvent)[0]] @@ -247,7 +247,7 @@ def updateEvent(self, event=None, home=None, cameratype=None): if not event: # If not event is provided we need to retrieve the oldest of # the last event seen by each camera - listEvent = dict() + listEvent = {} for cam_id in self.outdoor_lastEvent: listEvent[ self.outdoor_lastEvent[cam_id]["time"] @@ -288,8 +288,7 @@ def personSeenByCamera(self, name, home=None, camera=None, exclude=0): # Check in the last event is someone known has been seen if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cam_id]) - array_time_event.reverse() + array_time_event = sorted(self.events[cam_id], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False @@ -306,7 +305,7 @@ def personSeenByCamera(self, name, home=None, camera=None, exclude=0): return False def _knownPersons(self): - known_persons = dict() + known_persons = {} for p_id, p in self.persons.items(): if "pseudo" in p: known_persons[p_id] = p @@ -330,8 +329,7 @@ def someoneKnownSeen(self, home=None, camera=None, exclude=0): if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cam_id]) - array_time_event.reverse() + array_time_event = sorted(self.events[cam_id], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False @@ -359,8 +357,7 @@ def someoneUnknownSeen(self, home=None, camera=None, exclude=0): if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cam_id]) - array_time_event.reverse() + array_time_event = sorted(self.events[cam_id], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False @@ -388,8 +385,7 @@ def motionDetected(self, home=None, camera=None, exclude=0): if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cam_id]) - array_time_event.reverse() + array_time_event = sorted(self.events[cam_id], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False @@ -481,8 +477,7 @@ def moduleMotionDetected(self, module=None, home=None, camera=None, exclude=0): if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cam_id]) - array_time_event.reverse() + array_time_event = sorted(self.events[cam_id], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False @@ -512,8 +507,7 @@ def moduleOpened(self, module=None, home=None, camera=None, exclude=0): if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cam_id]) - array_time_event.reverse() + array_time_event = sorted(self.events[cam_id], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 4d37da93c..f1583e519 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -36,45 +36,45 @@ def __init__(self, authData): self.homes = {d["id"]: d for d in self.rawData} if not self.homes: raise NoDevice("No thermostat available") - self.modules = dict() - self.rooms = dict() - self.schedules = dict() - self.zones = dict() - self.setpoint_duration = dict() - for i in range(len(self.rawData)): - nameHome = self.rawData[i].get("name") + self.modules = {} + self.rooms = {} + self.schedules = {} + self.zones = {} + self.setpoint_duration = {} + for item in self.rawData: + nameHome = item.get("name") if not nameHome: - raise NoDevice('No key ["name"] in %s', self.rawData[i].keys()) - if "modules" in self.rawData[i]: + raise NoDevice('No key ["name"] in %s', item.keys()) + if "modules" in item: if nameHome not in self.modules: - self.modules[nameHome] = dict() - for m in self.rawData[i]["modules"]: + self.modules[nameHome] = {} + for m in item["modules"]: self.modules[nameHome][m["id"]] = m if nameHome not in self.rooms: - self.rooms[nameHome] = dict() + self.rooms[nameHome] = {} if nameHome not in self.schedules: - self.schedules[nameHome] = dict() + self.schedules[nameHome] = {} if nameHome not in self.zones: - self.zones[nameHome] = dict() + self.zones[nameHome] = {} if nameHome not in self.setpoint_duration: - self.setpoint_duration[nameHome] = dict() - if "therm_setpoint_default_duration" in self.rawData[i]: - self.setpoint_duration[nameHome] = self.rawData[i][ + self.setpoint_duration[nameHome] = {} + if "therm_setpoint_default_duration" in item: + self.setpoint_duration[nameHome] = item[ "therm_setpoint_default_duration" ] - if "rooms" in self.rawData[i]: - for r in self.rawData[i]["rooms"]: - self.rooms[nameHome][r["id"]] = r - if "therm_schedules" in self.rawData[i]: - self.default_home = self.rawData[i]["name"] - for s in self.rawData[i]["therm_schedules"]: - self.schedules[nameHome][s["id"]] = s - for t in range(len(self.rawData[i]["therm_schedules"])): - idSchedule = self.rawData[i]["therm_schedules"][t]["id"] - if idSchedule not in self.zones[nameHome]: - self.zones[nameHome][idSchedule] = dict() - for z in self.rawData[i]["therm_schedules"][t]["zones"]: - self.zones[nameHome][idSchedule][z["id"]] = z + if "rooms" in item: + for room in item["rooms"]: + self.rooms[nameHome][room["id"]] = room + if "therm_schedules" in item: + self.default_home = item["name"] + for schedule in item["therm_schedules"]: + self.schedules[nameHome][schedule["id"]] = schedule + for schedule in item["therm_schedules"]: + scheduleId = schedule["id"] + if scheduleId not in self.zones[nameHome]: + self.zones[nameHome][scheduleId] = {} + for zone in schedule["zones"]: + self.zones[nameHome][scheduleId][zone["id"]] = zone def homeById(self, hid): return None if hid not in self.homes else self.homes[hid] @@ -136,9 +136,8 @@ class HomeStatus(HomeData): def __init__(self, authData, home_id=None, home=None): self.getAuthToken = authData.accessToken - # print(self.modules()) self.home_data = HomeData(authData) - # print(home_data.modules) + if home_id: self.home_id = home_id LOG.debug("home_id", self.home_id) @@ -150,39 +149,35 @@ def __init__(self, authData, home_id=None, home=None): resp = postRequest(_GETHOMESTATUS_REQ, postParams) if ( - "body" in resp - and "errors" in resp["body"] + "errors" in resp or "body" not in resp or "home" not in resp["body"] ): raise NoDevice("No device found, errors in response") return None self.rawData = resp["body"]["home"] - self.rooms = dict() - self.thermostats = dict() - self.valves = dict() - self.relays = dict() + self.rooms = {} + self.thermostats = {} + self.valves = {} + self.relays = {} for r in self.rawData["rooms"]: self.rooms[r["id"]] = r - for t in range(len(self.rawData["modules"])): - if self.rawData["modules"][t]["type"] == "NATherm1": - thermostatId = self.rawData["modules"][t]["id"] + for module in self.rawData["modules"]: + if module["type"] == "NATherm1": + thermostatId = module["id"] if thermostatId not in self.thermostats: - self.thermostats[thermostatId] = dict() - self.thermostats[thermostatId] = self.rawData["modules"][t] - # self.thermostats[t['id']] = t - for v in range(len(self.rawData["modules"])): - if self.rawData["modules"][v]["type"] == "NRV": - valveId = self.rawData["modules"][v]["id"] + self.thermostats[thermostatId] = {} + self.thermostats[thermostatId] = module + elif module["type"] == "NRV": + valveId = module["id"] if valveId not in self.valves: - self.valves[valveId] = dict() - self.valves[valveId] = self.rawData["modules"][v] - for r in range(len(self.rawData["modules"])): - if self.rawData["modules"][r]["type"] == "NAPlug": - relayId = self.rawData["modules"][r]["id"] + self.valves[valveId] = {} + self.valves[valveId] = module + elif module["type"] == "NAPlug": + relayId = module["id"] if relayId not in self.relays: - self.relays[relayId] = dict() - self.relays[relayId] = self.rawData["modules"][r] + self.relays[relayId] = {} + self.relays[relayId] = module if self.rooms != {}: self.default_room = list(self.rooms.values())[0] if self.relays != {}: diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 9abb2d954..16d0f1eb8 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -31,18 +31,18 @@ def __init__(self, authData, urlReq=None): if not self.rawData: raise NoDevice("No weather station available") self.stations = {d["_id"]: d for d in self.rawData} - self.modules = dict() - for i in range(len(self.rawData)): - if "modules" not in self.rawData[i]: - self.rawData[i]["modules"] = [self.rawData[i]] - for m in self.rawData[i]["modules"]: + self.modules = {} + for item in self.rawData: + if "modules" not in item: + item["modules"] = [item] + for m in item["modules"]: if "module_name" not in m: if m["type"] == "NHC": m["module_name"] = m["station_name"] else: continue self.modules[m["_id"]] = m - self.modules[m["_id"]]["main_device"] = self.rawData[i]["_id"] + self.modules[m["_id"]]["main_device"] = item["_id"] self.default_station = list(self.stations.values())[0]["station_name"] def modulesNamesList(self, station=None): @@ -120,7 +120,7 @@ def lastData(self, station=None, exclude=0): # Breaking change from Netatmo : dashboard_data no longer available if station lost if not s or "dashboard_data" not in s: return None - lastD = dict() + lastD = {} # Define oldest acceptable sensor measure event limit = (time.time() - exclude) if exclude else 0 ds = s["dashboard_data"] diff --git a/smart_home/__init__.py b/smart_home/__init__.py index d9ac53a03..3858bb6e4 100644 --- a/smart_home/__init__.py +++ b/smart_home/__init__.py @@ -62,7 +62,7 @@ def getStationMinMaxTH(station=None, module=None): mname = devList.stationByName(station)["module_name"] lastD = devList.lastData(station) if mname == "*": - result = dict() + result = {} for m in lastD.keys(): if time.time() - lastD[m]["When"] > 3600: continue diff --git a/tests/test_smart_home_camera.py b/tests/test_smart_home_camera.py index 4b8d19978..4a238756b 100644 --- a/tests/test_smart_home_camera.py +++ b/tests/test_smart_home_camera.py @@ -133,6 +133,18 @@ def test_CameraData_cameraUrls(cameraHomeData, requests_mock): assert cameraHomeData.cameraUrls() == (vpn_url, local_url) +def test_CameraData_cameraUrls_disconnected(auth, requests_mock): + with open("fixtures/camera_home_data_disconnected.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Camera._GETHOMEDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + camera_data = smart_home.Camera.CameraData(auth) + assert camera_data.cameraUrls() == (None, None) + + @pytest.mark.parametrize( "home, expected", [ diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index 03f36fba0..02790053e 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -156,6 +156,37 @@ def test_HomeStatus(homeStatus): assert homeStatus.default_room == expexted +def test_HomeStatus_error_and_data(auth, requests_mock): + with open("fixtures/home_status_error_and_data.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Thermostat._GETHOMESTATUS_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with open("fixtures/home_data_simple.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Thermostat._GETHOMESDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + homeStatus = smart_home.Thermostat.HomeStatus(auth) + assert len(homeStatus.rooms) == 3 + assert homeStatus.default_room["id"] == "2746182631" + + expexted = { + "id": "2746182631", + "reachable": True, + "therm_measured_temperature": 19.8, + "therm_setpoint_temperature": 12, + "therm_setpoint_mode": "away", + "therm_setpoint_start_time": 1559229567, + "therm_setpoint_end_time": 0, + } + assert homeStatus.default_room == expexted + + def test_HomeStatus_error(auth, requests_mock): with open("fixtures/home_status_empty.json") as f: json_fixture = json.load(f) From 52d00f9c0ff1f9bbc5baa9d9ce968dedd945ca59 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 23 Jun 2019 21:27:27 +0200 Subject: [PATCH 112/333] v2.1.0 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 73eeae9cc..1571a6221 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="2.0.1", # Should be updated with new versions + version="2.1.0", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From f033396adf285336696c6df58d52b393e725a821 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 2 Jul 2019 13:40:49 +0200 Subject: [PATCH 113/333] Fix Home Coach issues (#70) * Return data for all stations if none is given * Return only the modules for a given station --- smart_home/WeatherStation.py | 63 +++++++++++++++++++----------- tests/test_smart_home_homecoach.py | 2 +- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 16d0f1eb8..f0057c4ac 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -46,10 +46,15 @@ def __init__(self, authData, urlReq=None): self.default_station = list(self.stations.values())[0]["station_name"] def modulesNamesList(self, station=None): - res = set([m["module_name"] for m in self.modules.values()]) if station: - res.add(self.stationByName(station)["module_name"]) + res = set() + s = self.stationByName(station) + if s is not None: + res.add(s["module_name"]) + for m in s["modules"]: + res.add(m["module_name"]) else: + res = set([m["module_name"] for m in self.modules.values()]) for id, station in self.stations.items(): res.add(station["module_name"]) return list(res) @@ -116,31 +121,43 @@ def monitoredConditions(self, module): return conditions def lastData(self, station=None, exclude=0): - s = self.stationByName(station) + if station is not None: + stations = [station] + else: + stations = [s["station_name"] for s in list(self.stations.values())] # Breaking change from Netatmo : dashboard_data no longer available if station lost - if not s or "dashboard_data" not in s: - return None lastD = {} - # Define oldest acceptable sensor measure event - limit = (time.time() - exclude) if exclude else 0 - ds = s["dashboard_data"] - if "module_name" in s and ds["time_utc"] > limit: - lastD[s["module_name"]] = ds.copy() - lastD[s["module_name"]]["When"] = lastD[s["module_name"]].pop("time_utc") - lastD[s["module_name"]]["wifi_status"] = s["wifi_status"] - for module in s["modules"]: - if "dashboard_data" not in module or "module_name" not in module: - continue - ds = module["dashboard_data"] - if "time_utc" in ds and ds["time_utc"] > limit: - lastD[module["module_name"]] = ds.copy() - lastD[module["module_name"]]["When"] = lastD[module["module_name"]].pop( + for st in stations: + s = self.stationByName(st) + if not s or "dashboard_data" not in s: + return None + # Define oldest acceptable sensor measure event + limit = (time.time() - exclude) if exclude else 0 + ds = s["dashboard_data"] + if "module_name" in s and ds["time_utc"] > limit: + lastD[s["module_name"]] = ds.copy() + lastD[s["module_name"]]["When"] = lastD[s["module_name"]].pop( "time_utc" ) - # For potential use, add battery and radio coverage information to module data if present - for i in ("rf_status", "battery_vp", "battery_percent", "wifi_status"): - if i in module: - lastD[module["module_name"]][i] = module[i] + lastD[s["module_name"]]["wifi_status"] = s["wifi_status"] + for module in s["modules"]: + if "dashboard_data" not in module or "module_name" not in module: + continue + ds = module["dashboard_data"] + if "time_utc" in ds and ds["time_utc"] > limit: + lastD[module["module_name"]] = ds.copy() + lastD[module["module_name"]]["When"] = lastD[ + module["module_name"] + ].pop("time_utc") + # For potential use, add battery and radio coverage information to module data if present + for i in ( + "rf_status", + "battery_vp", + "battery_percent", + "wifi_status", + ): + if i in module: + lastD[module["module_name"]][i] = module[i] return lastD def checkNotUpdated(self, station=None, delay=3600): diff --git a/tests/test_smart_home_homecoach.py b/tests/test_smart_home_homecoach.py index 7166e9c94..5b7a737f5 100644 --- a/tests/test_smart_home_homecoach.py +++ b/tests/test_smart_home_homecoach.py @@ -10,7 +10,7 @@ def test_HomeCoachData(homeCoachData): "station, expected", [ (None, ["Bedroom", "Kitchen", "Livingroom"]), - ("Bedroom", ["Bedroom", "Kitchen", "Livingroom"]), + ("Bedroom", ["Bedroom"]), pytest.param( "NoValidStation", None, From 413d38f462aa65c91b97bd6f57727c9c953e728e Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 10 Jul 2019 13:26:01 +0200 Subject: [PATCH 114/333] Add missing fixtures (#71) --- fixtures/camera_home_data_disconnected.json | 182 ++++++++++++++++++++ fixtures/home_status_error_and_data.json | 109 ++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 fixtures/camera_home_data_disconnected.json create mode 100644 fixtures/home_status_error_and_data.json diff --git a/fixtures/camera_home_data_disconnected.json b/fixtures/camera_home_data_disconnected.json new file mode 100644 index 000000000..c0ee0d810 --- /dev/null +++ b/fixtures/camera_home_data_disconnected.json @@ -0,0 +1,182 @@ +{ + "body": { + "homes": [ + { + "id": "91763b24c43d3e344f424e8b", + "name": "MYHOME", + "persons": [ + { + "id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "last_seen": 1557071156, + "out_of_sight": true, + "face": { + "id": "d74fad765b9100ef480720a9", + "version": 1, + "key": "a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7" + }, + "pseudo": "John Doe" + }, + { + "id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "last_seen": 1560600726, + "out_of_sight": true, + "face": { + "id": "d74fad765b9100ef480720a9", + "version": 3, + "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" + }, + "pseudo": "Jane Doe" + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff3", + "last_seen": 1560626666, + "out_of_sight": false, + "face": { + "id": "d74fad765b9100ef480720a9", + "version": 1, + "key": "a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" + }, + "pseudo": "Richard Doe" + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff4", + "last_seen": 1560621666, + "out_of_sight": true, + "face": { + "id": "d0ef44fad765b980720710a9", + "version": 1, + "key": "ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d0ef44fad765b980720710a9ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928" + } + } + ], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin" + }, + "cameras": [ + { + "id": "12:34:56:00:f1:62", + "type": "NACamera", + "status": "disconnected", + "sd_status": "on", + "alim_status": "on", + "name": "Hall", + "use_pin_code": false, + "last_setup": 1544828430 + } + ], + "smokedetectors": [], + "events": [ + { + "id": "a1b2c3d4e5f6abcdef123456", + "type": "person", + "time": 1560604700, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "video_status": "deleted", + "is_arrival": false, + "message": "John Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef123457", + "type": "person_away", + "time": 1560602400, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "message": "John Doe hat das Haus verlassen", + "sub_message": "John Doe gilt als abwesend, da das mit diesem Profil verbundene Telefon den Bereich des Hauses verlassen hat." + }, + { + "id": "a1b2c3d4e5f6abcdef123458", + "type": "person", + "time": 1560601200, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "video_status": "deleted", + "is_arrival": false, + "message": "John Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef123459", + "type": "person", + "time": 1560600100, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "snapshot": { + "id": "d74fad765b9100ef480720a9", + "version": 1, + "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" + }, + "video_id": "12345678-36bc-4b9a-9762-5194e707ed51", + "video_status": "available", + "is_arrival": false, + "message": "Jane Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef12345a", + "type": "person", + "time": 1560603600, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827375-7e04-5298-83ae-a0cb8372dff3", + "snapshot": { + "id": "532dde8d17554c022ab071b8", + "version": 1, + "key": "9fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28", + "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b89fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28" + }, + "video_id": "12345678-1234-46cb-ad8f-23d893874099", + "video_status": "available", + "is_arrival": false, + "message": "Bewegung erkannt" + }, + { + "id": "a1b2c3d4e5f6abcdef12345b", + "type": "movement", + "time": 1560506200, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "category": "human", + "snapshot": { + "id": "532dde8d17554c022ab071b9", + "version": 1, + "key": "8fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28", + "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b98fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28" + }, + "vignette": { + "id": "5dc021b5dea854bd2321707a", + "version": 1, + "key": "58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944", + "url": "https://netatmocameraimage.blob.core.windows.net/production/5dc021b5dea854bd2321707a58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944" + }, + "video_id": "12345678-1234-46cb-ad8f-23d89387409a", + "video_status": "available", + "message": "Bewegung erkannt" + } + ] + } + ], + "user": { + "reg_locale": "de-DE", + "lang": "de-DE", + "country": "DE", + "mail": "john@doe.com" + }, + "global_info": { + "show_tags": true + } + }, + "status": "ok", + "time_exec": 0.03621506690979, + "time_server": 1560626960 +} \ No newline at end of file diff --git a/fixtures/home_status_error_and_data.json b/fixtures/home_status_error_and_data.json new file mode 100644 index 000000000..49228b44d --- /dev/null +++ b/fixtures/home_status_error_and_data.json @@ -0,0 +1,109 @@ +{ + "status": "ok", + "time_server": 1559292039, + "body": { + "home": { + "modules": [ + { + "id": "12:34:56:00:fa:d0", + "type": "NAPlug", + "firmware_revision": 174, + "rf_strength": 107, + "wifi_strength": 42 + }, + { + "id": "12:34:56:00:01:ae", + "reachable": true, + "type": "NATherm1", + "firmware_revision": 65, + "rf_strength": 58, + "battery_level": 3793, + "boiler_valve_comfort_boost": false, + "boiler_status": false, + "anticipating": false, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "high" + }, + { + "id": "12:34:56:03:a5:54", + "reachable": true, + "type": "NRV", + "firmware_revision": 79, + "rf_strength": 51, + "battery_level": 3025, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "full" + }, + { + "id": "12:34:56:03:a0:ac", + "reachable": true, + "type": "NRV", + "firmware_revision": 79, + "rf_strength": 59, + "battery_level": 3029, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "full" + } + ], + "rooms": [ + { + "id": "2746182631", + "reachable": true, + "therm_measured_temperature": 19.8, + "therm_setpoint_temperature": 12, + "therm_setpoint_mode": "away", + "therm_setpoint_start_time": 1559229567, + "therm_setpoint_end_time": 0 + }, + { + "id": "2940411577", + "reachable": true, + "therm_measured_temperature": 27, + "heating_power_request": 0, + "therm_setpoint_temperature": 7, + "therm_setpoint_mode": "hg", + "therm_setpoint_start_time": 0, + "therm_setpoint_end_time": 0, + "anticipating": false, + "open_window": false + }, + { + "id": "2833524037", + "reachable": true, + "therm_measured_temperature": 24.5, + "heating_power_request": 0, + "therm_setpoint_temperature": 7, + "therm_setpoint_mode": "hg", + "therm_setpoint_start_time": 0, + "therm_setpoint_end_time": 0, + "anticipating": false, + "open_window": false + } + ], + "id": "91763b24c43d3e344f424e8b", + "persons": [ + { + "id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "last_seen": 1557071156, + "out_of_sight": true + }, + { + "id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "last_seen": 1559282761, + "out_of_sight": false + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff3", + "last_seen": 1559224132, + "out_of_sight": true + } + ] + }, + "errors": [ + { + "code": 6, + "id": "12:34:56:00:f1:62" + } + ] + } +} \ No newline at end of file From a054c49b8ca928326923fe46da939ebe092a438d Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 22 Jul 2019 22:36:48 +0200 Subject: [PATCH 115/333] Check for camera id before updating events (#67) --- smart_home/Camera.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index c489c0c55..fe77d597f 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -170,17 +170,13 @@ def cameraUrls(self, camera=None, home=None, cid=None): vpn_url = camera_data.get("vpn_url") if camera_data.get("is_local"): try: - resp = postRequest( - "{0}/command/ping".format(vpn_url), {} - ) + resp = postRequest("{0}/command/ping".format(vpn_url), {}) temp_local_url = resp["local_url"] except URLError: return None, None try: - resp = postRequest( - "{0}/command/ping".format(temp_local_url), {} - ) + resp = postRequest("{0}/command/ping".format(temp_local_url), {}) if temp_local_url == resp["local_url"]: local_url = temp_local_url except URLError: @@ -264,8 +260,12 @@ def updateEvent(self, event=None, home=None, cameratype=None): eventList = resp["body"]["events_list"] for e in eventList: if e["type"] == "outdoor": + if e["camera_id"] not in self.outdoor_events: + self.outdoor_events[e["camera_id"]] = {} self.outdoor_events[e["camera_id"]][e["time"]] = e elif e["type"] != "outdoor": + if e["camera_id"] not in self.events: + self.events[e["camera_id"]] = {} self.events[e["camera_id"]][e["time"]] = e for camera in self.events: self.lastEvent[camera] = self.events[camera][ From d38595ff3ff22dfff0839b19f1510a148a8598f6 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 22 Jul 2019 22:37:08 +0200 Subject: [PATCH 116/333] Fix missing attributes (#72) --- smart_home/WeatherStation.py | 7 +++++++ tests/test_smart_home_weatherstation.py | 13 ++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index f0057c4ac..eb442882d 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -118,6 +118,13 @@ def monitoredConditions(self, module): else: # assume all other modules have rf_status, battery_vp, and battery_percent conditions.extend(["rf_status", "battery_vp", "battery_percent"]) + if ( + mod["type"] == "NAMain" + or mod["type"] == "NHC" + or mod["type"] == "NAModule1" + or mod["type"] == "NAModule4" + ): + conditions.extend(["min_temp", "max_temp"]) return conditions def lastData(self, station=None, exclude=0): diff --git a/tests/test_smart_home_weatherstation.py b/tests/test_smart_home_weatherstation.py index f01605c31..936936ada 100644 --- a/tests/test_smart_home_weatherstation.py +++ b/tests/test_smart_home_weatherstation.py @@ -147,6 +147,8 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): "battery_vp", "co2", "humidity", + "max_temp", + "min_temp", "rf_status", "temperature", ], @@ -176,7 +178,16 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): ), ( "NetatmoIndoor", - ["co2", "humidity", "noise", "pressure", "temperature", "wifi_status"], + [ + "co2", + "humidity", + "max_temp", + "min_temp", + "noise", + "pressure", + "temperature", + "wifi_status", + ], ), pytest.param( "12:34:56:07:bb:3e", From 294d568015e936e505701d9dfc628c2c161ed143 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 23 Jun 2019 21:58:39 +0200 Subject: [PATCH 117/333] Blacken code Signed-off-by: Hugo D. (jabesq) --- setup.py | 6 ++++-- smart_home/Thermostat.py | 6 +----- tests/test_smart_home_homecoach.py | 1 - 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index 1571a6221..f92656c1f 100755 --- a/setup.py +++ b/setup.py @@ -16,7 +16,9 @@ data_files=[], url="https://github.com/jabesq/netatmo-api-python", license="MIT", - description="Simple API to access Netatmo weather station data from any Python 3 script. " - "Design for Home-Assitant (but not only)", + description=( + "Simple API to access Netatmo weather station data from any Python 3 script. " + "Design for Home-Assitant (but not only)" + ), long_description=open("README.md").read(), ) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index f1583e519..dc750f733 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -148,11 +148,7 @@ def __init__(self, authData, home_id=None, home=None): postParams = {"access_token": self.getAuthToken, "home_id": self.home_id} resp = postRequest(_GETHOMESTATUS_REQ, postParams) - if ( - "errors" in resp - or "body" not in resp - or "home" not in resp["body"] - ): + if "errors" in resp or "body" not in resp or "home" not in resp["body"]: raise NoDevice("No device found, errors in response") return None self.rawData = resp["body"]["home"] diff --git a/tests/test_smart_home_homecoach.py b/tests/test_smart_home_homecoach.py index 5b7a737f5..571a5e674 100644 --- a/tests/test_smart_home_homecoach.py +++ b/tests/test_smart_home_homecoach.py @@ -22,4 +22,3 @@ def test_HomeCoachData(homeCoachData): ) def test_HomeCoachData_modulesNamesList(homeCoachData, station, expected): assert sorted(homeCoachData.modulesNamesList(station)) == expected - From 0328e3bd48563d58edd0d042e58dad68dac5c179 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 23 Jun 2019 22:17:12 +0200 Subject: [PATCH 118/333] Add Pipfile to handle requirements Signed-off-by: Hugo D. (jabesq) --- Pipfile | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Pipfile diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000..48378d321 --- /dev/null +++ b/Pipfile @@ -0,0 +1,20 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +twine = "*" +pytest = "*" +black = "*" +mypy = "*" +flake8 = "*" +pylint = "*" + +[packages] + +[requires] +python_version = "3.7" + +[pipenv] +allow_prereleases = true From a9d828d07d1e71d6709e54ff0b8c6a13bb699915 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Tue, 2 Jul 2019 13:55:27 +0200 Subject: [PATCH 119/333] v2.1.2 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f92656c1f..0a7641be1 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="2.1.0", # Should be updated with new versions + version="2.1.2", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From 88b9d8f8909cf7a6742059acda554daf448ece15 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Mon, 22 Jul 2019 22:56:53 +0200 Subject: [PATCH 120/333] Add requirements for pytest Signed-off-by: Hugo D. (jabesq) --- Pipfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Pipfile b/Pipfile index 48378d321..9605f3ed7 100644 --- a/Pipfile +++ b/Pipfile @@ -10,6 +10,11 @@ black = "*" mypy = "*" flake8 = "*" pylint = "*" +docutils = "==0.14" +pytest-mock = "*" +pytest-cov = "*" +requests-mock = "*" +freezegun = "*" [packages] From beff0eb800f2cc644e2be1ea5130dc7df2cfe465 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 26 Jul 2019 13:37:51 +0200 Subject: [PATCH 121/333] Refactor to use IDs rather than names (#73) * Use id as module/room/schedule index instead of name * Add home_id as arguments * Add station_id to modulesNamesList() * Update camera test fixture * Update camera test cases * Index cameras by id * Set default home * Add station id parameter * Blacken code * Undo * Add getHomeName method * Introduce new exceptions * Remove print statement * Blacken code * Move exceptions into their own file * Update camera tests * Clean up camera and tests * Import exceptions --- fixtures/camera_home_data.json | 36 +++++++ fixtures/home_data_simple.json | 12 +++ pyatmo.py | 3 +- smart_home/Camera.py | 65 +++++++----- smart_home/Exceptions.py | 14 +++ smart_home/PublicData.py | 3 +- smart_home/Thermostat.py | 153 +++++++++++++++++----------- smart_home/WeatherStation.py | 24 +++-- smart_home/__init__.py | 4 - tests/test_smart_home_camera.py | 134 ++++++++++++------------ tests/test_smart_home_thermostat.py | 66 ++++++++++-- 11 files changed, 341 insertions(+), 173 deletions(-) create mode 100644 smart_home/Exceptions.py diff --git a/fixtures/camera_home_data.json b/fixtures/camera_home_data.json index 96f703a47..075fb8cf7 100644 --- a/fixtures/camera_home_data.json +++ b/fixtures/camera_home_data.json @@ -70,6 +70,18 @@ "name": "Hall", "use_pin_code": false, "last_setup": 1544828430 + }, + { + "id": "12:34:56:00:a5:a4", + "type": "NOC", + "status": "on", + "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,,", + "is_local": true, + "sd_status": "on", + "alim_status": "on", + "name": "Garden", + "last_setup": 1563737661, + "light_mode_status": "auto" } ], "smokedetectors": [], @@ -166,6 +178,30 @@ "message": "Bewegung erkannt" } ] + }, + { + "id": "91763b24c43d3e344f424e8c", + "persons": [], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin" + }, + "cameras": [ + { + "id": "12:34:56:00:a5:a5", + "type": "NOC", + "status": "on", + "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTz,,", + "is_local": true, + "sd_status": "on", + "alim_status": "on", + "name": "Street", + "last_setup": 1563737561, + "light_mode_status": "auto" + } + ], + "smokedetectors": [] } ], "user": { diff --git a/fixtures/home_data_simple.json b/fixtures/home_data_simple.json index abfcae61f..ea6bda4f6 100644 --- a/fixtures/home_data_simple.json +++ b/fixtures/home_data_simple.json @@ -565,6 +565,18 @@ } ], "therm_mode": "schedule" + }, + { + "id": "91763b24c43d3e344f424e8c", + "altitude": 112, + "coordinates": [ + 52.516263, + 13.377726 + ], + "country": "DE", + "timezone": "Europe/Berlin", + "therm_setpoint_default_duration": 180, + "therm_mode": "schedule" } ], "user": { diff --git a/pyatmo.py b/pyatmo.py index 449c20864..b99606a79 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -11,12 +11,13 @@ import logging import time -from smart_home import _BASE_URL, NoDevice, postRequest +from smart_home import _BASE_URL, postRequest from smart_home.Camera import CameraData from smart_home.HomeCoach import HomeCoachData from smart_home.PublicData import PublicData from smart_home.Thermostat import HomeData, HomeStatus from smart_home.WeatherStation import WeatherStationData +from smart_home.Exceptions import NoDevice, InvalidHome, InvalidRoom, NoSchedule LOG = logging.getLogger(__name__) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index fe77d597f..c2a4d19ea 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -3,7 +3,8 @@ import time from urllib.error import URLError -from . import _BASE_URL, NoDevice, postRequest +from . import _BASE_URL, postRequest +from .Exceptions import NoDevice, InvalidHome LOG = logging.getLogger(__name__) @@ -42,19 +43,25 @@ def __init__(self, authData, size=15): self.outdoor_lastEvent = {} self.types = {} self.default_home = None + self.default_home_id = None self.default_camera = None for item in self.rawData: + homeId = item.get("id") nameHome = item.get("name") if not nameHome: - raise NoDevice('No key ["name"] in %s' % item.keys()) - if nameHome not in self.cameras: - self.cameras[nameHome] = {} - if nameHome not in self.types: - self.types[nameHome] = {} + nameHome = "Unknown" + self.homes[homeId]["name"] = nameHome + if not homeId: + raise NoDevice('No key ["id"] in %s' % item.keys()) + if homeId not in self.cameras: + self.cameras[homeId] = {} + if homeId not in self.types: + self.types[homeId] = {} for p in item["persons"]: self.persons[p["id"]] = p if "events" in item: self.default_home = item["name"] + self.default_home_id = item["id"] for e in item["events"]: if e["type"] == "outdoor": if e["camera_id"] not in self.outdoor_events: @@ -65,13 +72,13 @@ def __init__(self, authData, size=15): self.events[e["camera_id"]] = {} self.events[e["camera_id"]][e["time"]] = e for c in item["cameras"]: - self.cameras[nameHome][c["id"]] = c + self.cameras[homeId][c["id"]] = c if c["type"] == "NACamera" and "modules" in c: for m in c["modules"]: self.modules[m["id"]] = m self.modules[m["id"]]["cam_id"] = c["id"] for t in item["cameras"]: - self.types[nameHome][t["type"]] = t + self.types[homeId][t["type"]] = t for camera in self.events: self.lastEvent[camera] = self.events[camera][ sorted(self.events[camera])[-1] @@ -85,7 +92,7 @@ def __init__(self, authData, size=15): else: self.default_module = None if self.default_home is not None and len(self.cameras) > 0: - self.default_camera = list(self.cameras[self.default_home].values())[0] + self.default_camera = list(self.cameras[self.default_home_id].values())[0] def homeById(self, hid): return None if hid not in self.homes else self.homes[hid] @@ -96,6 +103,7 @@ def homeByName(self, home=None): for key, value in self.homes.items(): if value["name"] == home: return self.homes[key] + raise InvalidHome() def cameraById(self, cid): for home, cam in self.cameras.items(): @@ -103,23 +111,34 @@ def cameraById(self, cid): return self.cameras[home][cid] return None - def cameraByName(self, camera=None, home=None): - if not camera and not home: + def cameraByName(self, camera=None, home=None, home_id=None): + if home_id is None: + if home is None: + hid = self.default_home_id + else: + try: + hid = self.homeByName(home)["id"] + except InvalidHome: + LOG.debug("Invalid home %s", home) + return None + else: + hid = home_id + if camera is None and home is None and home_id is None: return self.default_camera - elif home and camera: - if home not in self.cameras: - return None - for cam_id in self.cameras[home]: - if self.cameras[home][cam_id]["name"] == camera: - return self.cameras[home][cam_id] - elif not home and camera: - for home, cam_ids in self.cameras.items(): + elif not (home_id or home) and camera: + for h_id, cam_ids in self.cameras.items(): for cam_id in cam_ids: - if self.cameras[home][cam_id]["name"] == camera: - return self.cameras[home][cam_id] + if self.cameras[h_id][cam_id]["name"] == camera: + return self.cameras[h_id][cam_id] + elif hid and camera: + hid = self.homeByName(home)["id"] + if hid not in self.cameras: + return None + for cam_id in self.cameras[hid]: + if self.cameras[hid][cam_id]["name"] == camera: + return self.cameras[hid][cam_id] else: - return list(self.cameras[home].values())[0] - return None + return list(self.cameras[hid].values())[0] def moduleById(self, mid): return None if mid not in self.modules else self.modules[mid] diff --git a/smart_home/Exceptions.py b/smart_home/Exceptions.py new file mode 100644 index 000000000..651eb9829 --- /dev/null +++ b/smart_home/Exceptions.py @@ -0,0 +1,14 @@ +class NoSchedule(Exception): + pass + + +class InvalidHome(Exception): + pass + + +class InvalidRoom(Exception): + pass + + +class NoDevice(Exception): + pass diff --git a/smart_home/PublicData.py b/smart_home/PublicData.py index 97c2c53ca..aaae7d3f4 100644 --- a/smart_home/PublicData.py +++ b/smart_home/PublicData.py @@ -1,6 +1,7 @@ import time -from . import _BASE_URL, NoDevice, postRequest +from . import _BASE_URL, postRequest +from .Exceptions import NoDevice _GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata" _LON_NE = 6.221652 diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index dc750f733..24ab97ec4 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -1,6 +1,7 @@ import logging -from . import _BASE_URL, NoDevice, postRequest +from . import _BASE_URL, postRequest +from .Exceptions import NoDevice, NoSchedule, InvalidHome, InvalidRoom LOG = logging.getLogger(__name__) @@ -12,10 +13,6 @@ _SWITCHHOMESCHEDULE_REQ = _BASE_URL + "api/switchhomeschedule" -class NoSchedule(Exception): - pass - - class HomeData: """ List the Energy devices (relays, thermostat modules and valves) @@ -41,40 +38,47 @@ def __init__(self, authData): self.schedules = {} self.zones = {} self.setpoint_duration = {} + self.default_home = None + self.default_home_id = None for item in self.rawData: + idHome = item.get("id") + if not idHome: + raise NoDevice('No key ["id"] in %s', item.keys()) nameHome = item.get("name") if not nameHome: - raise NoDevice('No key ["name"] in %s', item.keys()) + nameHome = "Unknown" + self.homes[idHome]["name"] = nameHome if "modules" in item: - if nameHome not in self.modules: - self.modules[nameHome] = {} + if idHome not in self.modules: + self.modules[idHome] = {} for m in item["modules"]: - self.modules[nameHome][m["id"]] = m - if nameHome not in self.rooms: - self.rooms[nameHome] = {} - if nameHome not in self.schedules: - self.schedules[nameHome] = {} - if nameHome not in self.zones: - self.zones[nameHome] = {} - if nameHome not in self.setpoint_duration: - self.setpoint_duration[nameHome] = {} + self.modules[idHome][m["id"]] = m + if idHome not in self.rooms: + self.rooms[idHome] = {} + if idHome not in self.schedules: + self.schedules[idHome] = {} + if idHome not in self.zones: + self.zones[idHome] = {} + if idHome not in self.setpoint_duration: + self.setpoint_duration[idHome] = {} if "therm_setpoint_default_duration" in item: - self.setpoint_duration[nameHome] = item[ + self.setpoint_duration[idHome] = item[ "therm_setpoint_default_duration" ] if "rooms" in item: for room in item["rooms"]: - self.rooms[nameHome][room["id"]] = room + self.rooms[idHome][room["id"]] = room if "therm_schedules" in item: - self.default_home = item["name"] + self.default_home = nameHome + self.default_home_id = item["id"] for schedule in item["therm_schedules"]: - self.schedules[nameHome][schedule["id"]] = schedule + self.schedules[idHome][schedule["id"]] = schedule for schedule in item["therm_schedules"]: scheduleId = schedule["id"] - if scheduleId not in self.zones[nameHome]: - self.zones[nameHome][scheduleId] = {} + if scheduleId not in self.zones[idHome]: + self.zones[idHome][scheduleId] = {} for zone in schedule["zones"]: - self.zones[nameHome][scheduleId][zone["id"]] = zone + self.zones[idHome][scheduleId][zone["id"]] = zone def homeById(self, hid): return None if hid not in self.homes else self.homes[hid] @@ -85,6 +89,7 @@ def homeByName(self, home=None): for key, value in self.homes.items(): if value["name"] == home: return self.homes[key] + raise InvalidHome("Invalid Home %s" % home) def gethomeId(self, home=None): if not home: @@ -95,11 +100,27 @@ def gethomeId(self, home=None): LOG.debug(self.default_home) if "therm_schedules" in self.homes[key]: return self.homes[key]["id"] + raise InvalidHome("Invalid Home %s" % home) + + def getHomeName(self, home_id=None): + if home_id is None: + home_id = self.default_home_id + for key, value in self.homes.items(): + if value["id"] == home_id: + return self.homes[key]["name"] + raise InvalidHome("Invalid Home ID %s" % home_id) + + def getSelectedschedule(self, home=None, home_id=None): + if not home_id: + if not home: + home = self.default_home + home_id = self.gethomeId(home=home) + + try: + self.schedule = self.schedules[home_id] + except KeyError: + raise NoSchedule("No schedules available for %s" % home_id) - def getSelectedschedule(self, home=None): - if not home: - home = self.default_home - self.schedule = self.schedules[home] for key in self.schedule.keys(): if "selected" in self.schedule[key].keys(): return self.schedule[key] @@ -109,8 +130,8 @@ def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): home = self.default_home home_id = self.gethomeId(home=home) schedules = { - self.schedules[home][s]["name"]: self.schedules[home][s]["id"] - for s in self.schedules[home] + self.schedules[home_id][s]["name"]: self.schedules[home_id][s]["id"] + for s in self.schedules[home_id] } if schedule is None and schedule_id is not None: if schedule_id not in list(schedules.values()): @@ -138,10 +159,10 @@ def __init__(self, authData, home_id=None, home=None): self.getAuthToken = authData.accessToken self.home_data = HomeData(authData) - if home_id: + if home_id is not None: self.home_id = home_id LOG.debug("home_id", self.home_id) - elif home: + elif home is not None: self.home_id = self.home_data.gethomeId(home=home) else: self.home_id = self.home_data.gethomeId(home=self.home_data.default_home) @@ -156,9 +177,9 @@ def __init__(self, authData, home_id=None, home=None): self.thermostats = {} self.valves = {} self.relays = {} - for r in self.rawData["rooms"]: + for r in self.rawData.get("rooms", []): self.rooms[r["id"]] = r - for module in self.rawData["modules"]: + for module in self.rawData.get("modules", []): if module["type"] == "NATherm1": thermostatId = module["id"] if thermostatId not in self.thermostats: @@ -190,6 +211,7 @@ def roomById(self, rid): for key, value in self.rooms.items(): if value["id"] == rid: return self.rooms[key] + raise InvalidRoom("No room with ID %s" % rid) def thermostatById(self, rid): if not rid: @@ -197,6 +219,7 @@ def thermostatById(self, rid): for key, value in self.thermostats.items(): if value["id"] == rid: return self.thermostats[key] + raise InvalidRoom("No room with ID %s" % rid) def relayById(self, rid): if not rid: @@ -204,6 +227,7 @@ def relayById(self, rid): for key, value in self.relays.items(): if value["id"] == rid: return self.relays[key] + raise InvalidRoom("No room with ID %s" % rid) def valveById(self, rid): if not rid: @@ -211,16 +235,14 @@ def valveById(self, rid): for key, value in self.valves.items(): if value["id"] == rid: return self.valves[key] + raise InvalidRoom("No room with ID %s" % rid) def setPoint(self, rid=None): """ Return the setpoint of a given room. """ setpoint = None - if rid: - room_data = self.roomById(rid=rid) - else: - room_data = self.roomById(rid=None) + room_data = self.roomById(rid=rid) if room_data: setpoint = room_data["therm_setpoint_temperature"] return setpoint @@ -230,25 +252,43 @@ def setPointmode(self, rid=None): Return the setpointmode of a given room. """ setpointmode = None - if rid: + try: room_data = self.roomById(rid=rid) - else: - room_data = self.roomById(rid=None) + except InvalidRoom: + LOG.debug("Invalid room %s", rid) + room_data = None if room_data: setpointmode = room_data["therm_setpoint_mode"] return setpointmode - def getAwaytemp(self, home=None): - if not home: - home = self.home_data.default_home - LOG.debug(self.home_data.default_home) - data = self.home_data.getSelectedschedule(home=home) + def getAwaytemp(self, home=None, home_id=None): + if not home_id: + if not home: + home = self.home_data.default_home + LOG.debug(self.home_data.default_home) + try: + home_id = self.home_data.gethomeId(home) + except InvalidHome: + LOG.debug("No Schedule for Home ID %s", home_id) + return None + try: + data = self.home_data.getSelectedschedule(home_id=home_id) + except NoSchedule: + LOG.debug("No Schedule for Home ID %s", home_id) + return None return data["away_temp"] - def getHgtemp(self, home=None): - if not home: - home = self.home_data.default_home - data = self.home_data.getSelectedschedule(home=home) + def getHgtemp(self, home=None, home_id=None): + if not home_id: + if not home: + home = self.home_data.default_home + LOG.debug(self.home_data.default_home) + home_id = self.home_data.gethomeId(home) + try: + data = self.home_data.getSelectedschedule(home_id=home_id) + except NoSchedule: + LOG.debug("No Schedule for Home ID %s", home_id) + return None return data["hg_temp"] def measuredTemperature(self, rid=None): @@ -257,10 +297,7 @@ def measuredTemperature(self, rid=None): """ temperature = None LOG.debug(rid) - if rid: - room_data = self.roomById(rid=rid) - else: - room_data = self.roomById(rid=None) + room_data = self.roomById(rid=rid) if room_data: temperature = room_data["therm_measured_temperature"] return temperature @@ -276,11 +313,13 @@ def boilerStatus(self, rid=None): boiler_status = relay_status["boiler_status"] return boiler_status - def thermostatType(self, home, rid): + def thermostatType(self, home, rid, home_id=None): module_id = None - for key in self.home_data.rooms[home]: + if home_id is None: + home_id = self.home_data.gethomeId(home=home) + for key in self.home_data.rooms[home_id]: if key == rid: - for module_id in self.home_data.rooms[home][rid]["module_ids"]: + for module_id in self.home_data.rooms[home_id][rid]["module_ids"]: self.module_id = module_id if module_id in self.thermostats: return "NATherm1" diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index eb442882d..b18f86559 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -1,7 +1,8 @@ import logging import time -from . import _BASE_URL, NoDevice, postRequest, todayStamps +from . import _BASE_URL, postRequest, todayStamps +from .Exceptions import NoDevice LOG = logging.getLogger(__name__) @@ -45,16 +46,19 @@ def __init__(self, authData, urlReq=None): self.modules[m["_id"]]["main_device"] = item["_id"] self.default_station = list(self.stations.values())[0]["station_name"] - def modulesNamesList(self, station=None): - if station: - res = set() - s = self.stationByName(station) - if s is not None: - res.add(s["module_name"]) - for m in s["modules"]: - res.add(m["module_name"]) + def modulesNamesList(self, station=None, station_id=None): + res = set() + station_data = None + if station_id is not None: + station_data = self.stationById(station_id) + elif station is not None: + station_data = self.stationByName(station) + if station_data is not None: + res.add(station_data["module_name"]) + for m in station_data["modules"]: + res.add(m["module_name"]) else: - res = set([m["module_name"] for m in self.modules.values()]) + res.update([m["module_name"] for m in self.modules.values()]) for id, station in self.stations.items(): res.add(station["module_name"]) return list(res) diff --git a/smart_home/__init__.py b/smart_home/__init__.py index 3858bb6e4..de79695c0 100644 --- a/smart_home/__init__.py +++ b/smart_home/__init__.py @@ -9,10 +9,6 @@ _BASE_URL = "https://api.netatmo.com/" -class NoDevice(Exception): - pass - - # Utilities routines diff --git a/tests/test_smart_home_camera.py b/tests/test_smart_home_camera.py index 4a238756b..6428fcb48 100644 --- a/tests/test_smart_home_camera.py +++ b/tests/test_smart_home_camera.py @@ -10,6 +10,9 @@ import smart_home.Camera +INVALID_NAME = "InvalidName" + + def test_CameraData(cameraHomeData): assert cameraHomeData.default_home == "MYHOME" assert cameraHomeData.default_camera["id"] == "12:34:56:00:f1:62" @@ -20,15 +23,15 @@ def test_CameraData(cameraHomeData): "hid, expected", [ ("91763b24c43d3e344f424e8b", "MYHOME"), - pytest.param( - None, - None, - marks=pytest.mark.xfail(reason="Invalid home id not handled yet"), - ), + (INVALID_NAME, "MYHOME"), + pytest.param(None, None), ], ) def test_CameraData_homeById(cameraHomeData, hid, expected): - assert cameraHomeData.homeById(hid)["name"] == expected + if hid is None or hid == INVALID_NAME: + assert cameraHomeData.homeById(hid) is None + else: + assert cameraHomeData.homeById(hid)["name"] == expected @pytest.mark.parametrize( @@ -37,19 +40,25 @@ def test_CameraData_homeById(cameraHomeData, hid, expected): ("MYHOME", "91763b24c43d3e344f424e8b"), (None, "91763b24c43d3e344f424e8b"), ("", "91763b24c43d3e344f424e8b"), - pytest.param( - "InvalidName", - None, - marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), - ), + pytest.param(INVALID_NAME, None), ], ) def test_CameraData_homeByName(cameraHomeData, name, expected): - assert cameraHomeData.homeByName(name)["id"] == expected + if name == INVALID_NAME: + with pytest.raises(smart_home.Exceptions.InvalidHome): + assert cameraHomeData.homeByName(name) + else: + assert cameraHomeData.homeByName(name)["id"] == expected @pytest.mark.parametrize( - "cid, expected", [("12:34:56:00:f1:62", "Hall"), ("None", None), (None, None)] + "cid, expected", + [ + ("12:34:56:00:f1:62", "Hall"), + ("12:34:56:00:a5:a4", "Garden"), + ("None", None), + (None, None), + ], ) def test_CameraData_cameraById(cameraHomeData, cid, expected): camera = cameraHomeData.cameraById(cid) @@ -60,30 +69,38 @@ def test_CameraData_cameraById(cameraHomeData, cid, expected): @pytest.mark.parametrize( - "name, home, expected", + "name, home, home_id, expected", [ - ("Hall", None, "12:34:56:00:f1:62"), - (None, None, "12:34:56:00:f1:62"), - ("", None, "12:34:56:00:f1:62"), - ("Hall", "MYHOME", "12:34:56:00:f1:62"), - (None, "MYHOME", "12:34:56:00:f1:62"), - ("", "MYHOME", "12:34:56:00:f1:62"), - pytest.param( - "InvalidName", - None, - None, - marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), - ), - pytest.param( - None, - "InvalidName", - None, - marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), - ), + ("Hall", None, None, "12:34:56:00:f1:62"), + (None, None, None, "12:34:56:00:f1:62"), + ("", None, None, "12:34:56:00:f1:62"), + ("Hall", "MYHOME", None, "12:34:56:00:f1:62"), + ("Hall", None, "91763b24c43d3e344f424e8b", "12:34:56:00:f1:62"), + (None, None, "91763b24c43d3e344f424e8b", "12:34:56:00:f1:62"), + (None, "MYHOME", None, "12:34:56:00:f1:62"), + ("", "MYHOME", None, "12:34:56:00:f1:62"), + ("Garden", "MYHOME", None, "12:34:56:00:a5:a4"), + (INVALID_NAME, None, None, None), + (None, INVALID_NAME, None, None), ], ) -def test_CameraData_cameraByName(cameraHomeData, name, home, expected): - assert cameraHomeData.cameraByName(name, home)["id"] == expected +def test_CameraData_cameraByName(cameraHomeData, name, home, home_id, expected): + if home == INVALID_NAME or name == INVALID_NAME: + assert cameraHomeData.cameraByName(name, home, home_id) is None + elif home_id is None: + assert cameraHomeData.cameraByName(name, home)["id"] == expected + elif home is None: + assert cameraHomeData.cameraByName(name, home_id=home_id)["id"] == expected + else: + assert cameraHomeData.cameraByName(name, home, home_id)["id"] == expected + + +def test_CameraData_moduleById(cameraHomeData): + assert cameraHomeData.moduleById("00:00:00:00:00:00") is None + + +def test_CameraData_moduleByName(cameraHomeData): + assert cameraHomeData.moduleByName() is None @pytest.mark.parametrize( @@ -95,14 +112,9 @@ def test_CameraData_cameraByName(cameraHomeData, name, home, expected): (None, "MYHOME", None, "NACamera"), (None, "MYHOME", "12:34:56:00:f1:62", "NACamera"), (None, None, "12:34:56:00:f1:62", "NACamera"), - ("InvalidName", None, None, None), - pytest.param( - None, - "InvalidName", - None, - "NACamera", - marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), - ), + ("Garden", None, None, "NOC"), + (INVALID_NAME, None, None, None), + pytest.param(None, INVALID_NAME, None, None), ], ) def test_CameraData_cameraType(cameraHomeData, camera, home, cid, expected): @@ -151,14 +163,18 @@ def test_CameraData_cameraUrls_disconnected(auth, requests_mock): (None, ["Richard Doe"]), ("MYHOME", ["Richard Doe"]), pytest.param( - "InvalidHome", + INVALID_NAME, None, - marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), + # marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), ), ], ) def test_CameraData_personsAtHome(cameraHomeData, home, expected): - assert cameraHomeData.personsAtHome(home) == expected + if home == INVALID_NAME: + with pytest.raises(smart_home.Exceptions.InvalidHome): + assert cameraHomeData.personsAtHome(home) + else: + assert cameraHomeData.personsAtHome(home) == expected @freeze_time("2019-06-16") @@ -198,13 +214,7 @@ def test_CameraData_knownPersonsNames(cameraHomeData): (None, None, None, True), (None, None, 5, False), (None, "InvalidCamera", None, False), - pytest.param( - "InvalidHome", - None, - None, - True, - marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), - ), + ("InvalidHome", None, None, False), ], ) def test_CameraData_someoneKnownSeen(cameraHomeData, home, camera, exclude, expected): @@ -217,14 +227,8 @@ def test_CameraData_someoneKnownSeen(cameraHomeData, home, camera, exclude, expe [ (None, None, None, False), (None, None, 100, False), - (None, "InvalidCamera", None, False), - pytest.param( - "InvalidHome", - None, - None, - False, - marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), - ), + (None, INVALID_NAME, None, False), + (INVALID_NAME, None, None, False), ], ) def test_CameraData_someoneUnknownSeen(cameraHomeData, home, camera, exclude, expected): @@ -238,14 +242,8 @@ def test_CameraData_someoneUnknownSeen(cameraHomeData, home, camera, exclude, ex (None, None, None, False), (None, None, 140000, True), (None, None, 130000, False), - (None, "InvalidCamera", None, False), - pytest.param( - "InvalidHome", - None, - None, - False, - marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), - ), + (None, INVALID_NAME, None, False), + (INVALID_NAME, None, None, False), ], ) def test_CameraData_motionDetected(cameraHomeData, home, camera, exclude, expected): diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index 02790053e..07ac0fb18 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -7,13 +7,15 @@ from .conftest import does_not_raise import smart_home.Thermostat +import smart_home.Exceptions def test_HomeData(homeData): assert homeData.default_home == "MYHOME" - assert len(homeData.rooms[homeData.default_home]) == 4 + assert homeData.default_home_id == "91763b24c43d3e344f424e8b" + assert len(homeData.rooms[homeData.default_home_id]) == 4 - assert len(homeData.modules[homeData.default_home]) == 5 + assert len(homeData.modules[homeData.default_home_id]) == 5 expected = { "12:34:56:00:fa:d0": { @@ -59,7 +61,7 @@ def test_HomeData(homeData): "room_id": "3688132631", }, } - assert homeData.modules[homeData.default_home] == expected + assert homeData.modules[homeData.default_home_id] == expected def test_HomeData_no_data(auth, requests_mock): @@ -88,39 +90,57 @@ def test_HomeData_no_home_name(auth, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(smart_home.PublicData.NoDevice): - assert smart_home.Thermostat.HomeData(auth) + homeData = smart_home.Thermostat.HomeData(auth) + home_id = "91763b24c43d3e344f424e8b" + assert homeData.homeById(home_id)["name"] == "Unknown" def test_HomeData_homeById(homeData): home_id = "91763b24c43d3e344f424e8b" assert homeData.homeById(home_id)["name"] == "MYHOME" + home_id = "91763b24c43d3e344f424e8c" + assert homeData.homeById(home_id)["name"] == "Unknown" def test_HomeData_homeByName(homeData): assert homeData.homeByName()["name"] == "MYHOME" + assert homeData.homeByName()["id"] == "91763b24c43d3e344f424e8b" def test_HomeData_gethomeId(homeData): assert homeData.gethomeId() == "91763b24c43d3e344f424e8b" + assert homeData.gethomeId("MYHOME") == "91763b24c43d3e344f424e8b" + with pytest.raises(smart_home.Thermostat.InvalidHome): + assert homeData.gethomeId("InvalidName") + + +def test_HomeData_getHomeName(homeData): + assert homeData.getHomeName() == "MYHOME" + home_id = "91763b24c43d3e344f424e8b" + assert homeData.getHomeName(home_id) == "MYHOME" + home_id = "91763b24c43d3e344f424e8c" + assert homeData.getHomeName(home_id) == "Unknown" def test_HomeData_getSelectedschedule(homeData): assert homeData.getSelectedschedule()["name"] == "Default" + assert homeData.getSelectedschedule("MYHOME")["name"] == "Default" + with pytest.raises(smart_home.Exceptions.InvalidHome): + assert homeData.getSelectedschedule("Unknown") @pytest.mark.parametrize( "t_home, t_sched_id, t_sched, expected", [ - (None, None, None, pytest.raises(smart_home.Thermostat.NoSchedule)), + (None, None, None, pytest.raises(smart_home.Exceptions.NoSchedule)), (None, None, "Default", does_not_raise()), (None, "591b54a2764ff4d50d8b5795", None, does_not_raise()), - (None, None, "Summer", pytest.raises(smart_home.Thermostat.NoSchedule)), + (None, None, "Summer", pytest.raises(smart_home.Exceptions.NoSchedule)), ( None, "123456789abcdefg12345678", None, - pytest.raises(smart_home.Thermostat.NoSchedule), + pytest.raises(smart_home.Exceptions.NoSchedule), ), ], ) @@ -202,7 +222,7 @@ def test_HomeStatus_error(auth, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(smart_home.Thermostat.NoDevice): + with pytest.raises(smart_home.Exceptions.NoDevice): assert smart_home.Thermostat.HomeStatus(auth) @@ -217,6 +237,8 @@ def test_HomeStatus_roomById(homeStatus): "therm_setpoint_end_time": 0, } assert homeStatus.roomById("2746182631") == expexted + with pytest.raises(smart_home.Exceptions.InvalidRoom): + assert homeStatus.roomById("0000000000") def test_HomeStatus_thermostatById(homeStatus): @@ -234,6 +256,8 @@ def test_HomeStatus_thermostatById(homeStatus): "battery_state": "high", } assert homeStatus.thermostatById("12:34:56:00:01:ae") == expexted + with pytest.raises(smart_home.Exceptions.InvalidRoom): + assert homeStatus.thermostatById("00:00:00:00:00:00") def test_HomeStatus_relayById(homeStatus): @@ -245,6 +269,8 @@ def test_HomeStatus_relayById(homeStatus): "wifi_strength": 42, } assert homeStatus.relayById("12:34:56:00:fa:d0") == expexted + with pytest.raises(smart_home.Exceptions.InvalidRoom): + assert homeStatus.relayById("00:00:00:00:00:00") def test_HomeStatus_valveById(homeStatus): @@ -259,26 +285,45 @@ def test_HomeStatus_valveById(homeStatus): "battery_state": "full", } assert homeStatus.valveById("12:34:56:03:a5:54") == expexted + with pytest.raises(smart_home.Exceptions.InvalidRoom): + assert homeStatus.valveById("00:00:00:00:00:00") def test_HomeStatus_setPoint(homeStatus): + assert homeStatus.setPoint() == 12 assert homeStatus.setPoint("2746182631") == 12 + with pytest.raises(smart_home.Exceptions.InvalidRoom): + assert homeStatus.setPoint("0000000000") def test_HomeStatus_setPointmode(homeStatus): + assert homeStatus.setPointmode() == "away" assert homeStatus.setPointmode("2746182631") == "away" + assert homeStatus.setPointmode("0000000000") is None def test_HomeStatus_getAwaytemp(homeStatus): assert homeStatus.getAwaytemp() == 14 + assert homeStatus.getAwaytemp("MYHOME") == 14 + assert homeStatus.getAwaytemp("InvalidName") is None + assert homeStatus.getAwaytemp(home_id="91763b24c43d3e344f424e8b") == 14 + assert homeStatus.getAwaytemp(home_id="00000000000000000000000") is None def test_HomeStatus_getHgtemp(homeStatus): assert homeStatus.getHgtemp() == 7 + assert homeStatus.getHgtemp("MYHOME") == 7 + with pytest.raises(smart_home.Exceptions.InvalidHome): + assert homeStatus.getHgtemp("InvalidHome") + assert homeStatus.getHgtemp(home_id="91763b24c43d3e344f424e8b") == 7 + assert homeStatus.getHgtemp(home_id="00000000000000000000000") is None def test_HomeStatus_measuredTemperature(homeStatus): assert homeStatus.measuredTemperature() == 19.8 + assert homeStatus.measuredTemperature("2746182631") == 19.8 + with pytest.raises(smart_home.Exceptions.InvalidRoom): + assert homeStatus.measuredTemperature("0000000000") def test_HomeStatus_boilerStatus(homeStatus): @@ -288,6 +333,9 @@ def test_HomeStatus_boilerStatus(homeStatus): def test_HomeStatus_thermostatType(homeStatus): assert homeStatus.thermostatType("MYHOME", "2746182631") == "NATherm1" assert homeStatus.thermostatType("MYHOME", "2833524037") == "NRV" + with pytest.raises(smart_home.Exceptions.InvalidHome): + assert homeStatus.thermostatType("InvalidHome", "2833524037") + assert homeStatus.thermostatType("MYHOME", "0000000000") is None @pytest.mark.parametrize( From 44b43ec8c90d50aa6353f2482807d44850edbdf4 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Tue, 30 Jul 2019 21:40:30 +0200 Subject: [PATCH 122/333] v2.2.0 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0a7641be1..5beb5f5bf 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="2.1.2", # Should be updated with new versions + version="2.2.0", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From 11f53e5e888a55ed203baa736518714248e7dd03 Mon Sep 17 00:00:00 2001 From: Luis Martinez de Bartolome Izquierdo Date: Wed, 31 Jul 2019 08:38:41 +0200 Subject: [PATCH 123/333] Fix camera detection (#74) * Fix camera * fix camera * Update smart_home/Camera.py Co-Authored-By: cgtobi --- smart_home/Camera.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index c2a4d19ea..7c95e9b7e 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -52,7 +52,8 @@ def __init__(self, authData, size=15): nameHome = "Unknown" self.homes[homeId]["name"] = nameHome if not homeId: - raise NoDevice('No key ["id"] in %s' % item.keys()) + LOG.error('No key ["id"] in %s', item.keys()) + continue if homeId not in self.cameras: self.cameras[homeId] = {} if homeId not in self.types: From 937ab37e377c674af97bb8aec641ecf72bab9b56 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 1 Aug 2019 12:34:16 +0200 Subject: [PATCH 124/333] Replace exception (#75) --- smart_home/Thermostat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index 24ab97ec4..c55eb1168 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -43,7 +43,8 @@ def __init__(self, authData): for item in self.rawData: idHome = item.get("id") if not idHome: - raise NoDevice('No key ["id"] in %s', item.keys()) + LOG.error('No key ["id"] in %s', item.keys()) + continue nameHome = item.get("name") if not nameHome: nameHome = "Unknown" From 3703c6b7f55cb9d7eb0e04984bc5dbe547078c58 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Wed, 25 Sep 2019 07:55:20 -0400 Subject: [PATCH 125/333] Add support for reachable status (#76) * Add support for reachable * Exclude reachable support for HomeCoach * Use consistent formatting * Bug fixes --- smart_home/WeatherStation.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index b18f86559..ff7cdb3fe 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -116,19 +116,16 @@ def monitoredConditions(self, module): conditions.extend(["Rain", "sum_rain_24", "sum_rain_1"]) else: conditions.append(cond.lower()) - if mod["type"] == "NAMain" or mod["type"] == "NHC": + if mod["type"] in ["NAMain", "NHC"]: # the main module has wifi_status conditions.append("wifi_status") else: # assume all other modules have rf_status, battery_vp, and battery_percent conditions.extend(["rf_status", "battery_vp", "battery_percent"]) - if ( - mod["type"] == "NAMain" - or mod["type"] == "NHC" - or mod["type"] == "NAModule1" - or mod["type"] == "NAModule4" - ): + if mod["type"] in ["NAMain", "NAModule1", "NAModule4", "NHC"]: conditions.extend(["min_temp", "max_temp"]) + if mod["type"] in ["NAMain", "NAModule1", "NAModule2", "NAModule3", "NAModule4"]: + conditions.append("reachable") return conditions def lastData(self, station=None, exclude=0): @@ -165,6 +162,7 @@ def lastData(self, station=None, exclude=0): "rf_status", "battery_vp", "battery_percent", + "reachable", "wifi_status", ): if i in module: From 42f73f7a0e7f0021f4f1d3b991a59b0af2241e8c Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 2 Oct 2019 20:36:44 +0200 Subject: [PATCH 126/333] Add home_id as argument for camera methods (#77) * Add getHomeName method to Camera * Add gethomeId method for Camera * Add camera id argument to motion detection methods * Fix camera url retrieval and add tests --- fixtures/weatherstation_data_simple.json | 2 +- smart_home/Camera.py | 182 +++++++++++++---------- tests/test_smart_home_camera.py | 15 ++ tests/test_smart_home_homecoach.py | 16 ++ tests/test_smart_home_weatherstation.py | 8 + 5 files changed, 146 insertions(+), 77 deletions(-) diff --git a/fixtures/weatherstation_data_simple.json b/fixtures/weatherstation_data_simple.json index 19aa82b4b..d4b975103 100644 --- a/fixtures/weatherstation_data_simple.json +++ b/fixtures/weatherstation_data_simple.json @@ -41,7 +41,7 @@ "Pressure": 1017.3, "AbsolutePressure": 939.7, "min_temp": 23.4, - "max_temp": 24.6, + "max_temp": 25.6, "date_min_temp": 1559371924, "date_max_temp": 1559411964, "temp_trend": "stable", diff --git a/smart_home/Camera.py b/smart_home/Camera.py index 7c95e9b7e..81908c864 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -106,6 +106,24 @@ def homeByName(self, home=None): return self.homes[key] raise InvalidHome() + def getHomeName(self, home_id=None): + if home_id is None: + home_id = self.default_home_id + for key, value in self.homes.items(): + if value["id"] == home_id: + return self.homes[key]["name"] + raise InvalidHome("Invalid Home ID %s" % home_id) + + def gethomeId(self, home=None): + if not home: + home = self.default_home + for key, value in self.homes.items(): + if value["name"] == home: + LOG.debug(self.homes[key]["id"]) + LOG.debug(self.default_home) + return self.homes[key]["id"] + raise InvalidHome("Invalid Home %s" % home) + def cameraById(self, cid): for home, cam in self.cameras.items(): if cid in self.cameras[home]: @@ -162,7 +180,7 @@ def moduleByName(self, module=None, camera=None, home=None): return self.modules[key] return None - def cameraType(self, camera=None, home=None, cid=None): + def cameraType(self, camera=None, home=None, cid=None, home_id=None): """ Return the type of a given camera. """ @@ -170,12 +188,12 @@ def cameraType(self, camera=None, home=None, cid=None): if cid: camera_data = self.cameraById(cid) else: - camera_data = self.cameraByName(camera=camera, home=home) + camera_data = self.cameraByName(camera=camera, home=home, home_id=home_id) if camera_data: cameratype = camera_data["type"] return cameratype - def cameraUrls(self, camera=None, home=None, cid=None): + def cameraUrls(self, camera=None, home=None, cid=None, home_id=None): """ Return the vpn_url and the local_url (if available) of a given camera in order to access to its live feed @@ -184,8 +202,11 @@ def cameraUrls(self, camera=None, home=None, cid=None): vpn_url = None if cid: camera_data = self.cameraById(cid) + elif home_id: + camera_data = self.cameraByName(camera=camera, home_id=home_id) else: camera_data = self.cameraByName(camera=camera, home=home) + if camera_data: vpn_url = camera_data.get("vpn_url") if camera_data.get("is_local"): @@ -243,12 +264,16 @@ def getProfileImage(self, name): return self.getCameraPicture(image_id, key) return None, None - def updateEvent(self, event=None, home=None, cameratype=None): + def updateEvent(self, event=None, home=None, cameratype=None, home_id=None): """ Update the list of event with the latest ones """ - if not home: - home = self.default_home + if not home_id: + try: + home_id = self.gethomeId(home) + except InvalidHome: + LOG.debug("No valid Home %s", home) + return None if cameratype == "NACamera": # for the Welcome camera if not event: @@ -270,10 +295,9 @@ def updateEvent(self, event=None, home=None, cameratype=None): ] = self.outdoor_lastEvent[cam_id] event = listEvent[sorted(listEvent)[0]] - home_data = self.homeByName(home) postParams = { "access_token": self.getAuthToken, - "home_id": home_data["id"], + "home_id": home_id, "event_id": event["id"], } resp = postRequest(_GETEVENTSUNTIL_REQ, postParams) @@ -337,146 +361,152 @@ def knownPersonsNames(self): names.append(p["pseudo"]) return names - def someoneKnownSeen(self, home=None, camera=None, exclude=0): + def someoneKnownSeen(self, home=None, camera=None, exclude=0, cid=None): """ Return True if someone known has been seen """ - try: - cam_id = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("someoneKnownSeen: Camera name or home is unknown") - return False + if not cid: + try: + cid = self.cameraByName(camera=camera, home=home)["id"] + except TypeError: + LOG.error("someoneKnownSeen: Camera name or home is unknown") + return False if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cam_id], reverse=True) + array_time_event = sorted(self.events[cid], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False - elif self.events[cam_id][time_ev]["type"] == "person": - if ( - self.events[cam_id][time_ev]["person_id"] - in self._knownPersons() - ): + elif self.events[cid][time_ev]["type"] == "person": + if self.events[cid][time_ev]["person_id"] in self._knownPersons(): return True # Check in the last event is someone known has been seen - elif self.lastEvent[cam_id]["type"] == "person": - if self.lastEvent[cam_id]["person_id"] in self._knownPersons(): + elif self.lastEvent[cid]["type"] == "person": + if self.lastEvent[cid]["person_id"] in self._knownPersons(): return True return False - def someoneUnknownSeen(self, home=None, camera=None, exclude=0): + def someoneUnknownSeen(self, home=None, camera=None, exclude=0, cid=None): """ Return True if someone unknown has been seen """ - try: - cam_id = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("someoneUnknownSeen: Camera name or home is unknown") - return False + if not cid: + try: + cid = self.cameraByName(camera=camera, home=home)["id"] + except TypeError: + LOG.error("someoneUnknownSeen: Camera name or home is unknown") + return False if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cam_id], reverse=True) + array_time_event = sorted(self.events[cid], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False - elif self.events[cam_id][time_ev]["type"] == "person": + elif self.events[cid][time_ev]["type"] == "person": if ( - self.events[cam_id][time_ev]["person_id"] + self.events[cid][time_ev]["person_id"] not in self._knownPersons() ): return True # Check in the last event is someone known has been seen - elif self.lastEvent[cam_id]["type"] == "person": - if self.lastEvent[cam_id]["person_id"] not in self._knownPersons(): + elif self.lastEvent[cid]["type"] == "person": + if self.lastEvent[cid]["person_id"] not in self._knownPersons(): return True return False - def motionDetected(self, home=None, camera=None, exclude=0): + def motionDetected(self, home=None, camera=None, exclude=0, cid=None): """ Return True if movement has been detected """ - try: - cam_id = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("motionDetected: Camera name or home is unknown") - return False + if not cid: + try: + cid = self.cameraByName(camera=camera, home=home)["id"] + except TypeError: + LOG.error("motionDetected: Camera name or home is unknown") + return False if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cam_id], reverse=True) + array_time_event = sorted(self.events[cid], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False - elif self.events[cam_id][time_ev]["type"] == "movement": + elif self.events[cid][time_ev]["type"] == "movement": return True - elif self.lastEvent[cam_id]["type"] == "movement": + elif self.lastEvent[cid]["type"] == "movement": return True return False - def outdoormotionDetected(self, home=None, camera=None, offset=0): + def outdoormotionDetected(self, home=None, camera=None, offset=0, cid=None): """ Return True if outdoor movement has been detected """ - try: - cam_id = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("outdoormotionDetected: Camera name or home is unknown") - return False - if cam_id in self.lastEvent: - if self.lastEvent[cam_id]["type"] == "movement": - if self.lastEvent[cam_id][ + if not cid: + try: + cid = self.cameraByName(camera=camera, home=home)["id"] + except TypeError: + LOG.error("outdoormotionDetected: Camera name or home is unknown") + return False + + if cid in self.lastEvent: + if self.lastEvent[cid]["type"] == "movement": + if self.lastEvent[cid][ "video_status" - ] == "recording" and self.lastEvent[cam_id]["time"] + offset > int( + ] == "recording" and self.lastEvent[cid]["time"] + offset > int( time.time() ): return True return False - def humanDetected(self, home=None, camera=None, offset=0): + def humanDetected(self, home=None, camera=None, offset=0, cid=None): """ Return True if a human has been detected """ - try: - cam_id = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("personSeenByCamera: Camera name or home is unknown") - return False - if self.outdoor_lastEvent[cam_id]["video_status"] == "recording": - for e in self.outdoor_lastEvent[cam_id]["event_list"]: + if not cid: + try: + cid = self.cameraByName(camera=camera, home=home)["id"] + except TypeError: + LOG.error("personSeenByCamera: Camera name or home is unknown") + return False + + if self.outdoor_lastEvent[cid]["video_status"] == "recording": + for e in self.outdoor_lastEvent[cid]["event_list"]: if e["type"] == "human" and e["time"] + offset > int(time.time()): return True return False - def animalDetected(self, home=None, camera=None, offset=0): + def animalDetected(self, home=None, camera=None, offset=0, cid=None): """ Return True if an animal has been detected """ - try: - cam_id = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("animalDetected: Camera name or home is unknown") - return False + if not cid: + try: + cid = self.cameraByName(camera=camera, home=home)["id"] + except TypeError: + LOG.error("animalDetected: Camera name or home is unknown") + return False - if self.outdoor_lastEvent[cam_id]["video_status"] == "recording": - for e in self.outdoor_lastEvent[cam_id]["event_list"]: + if self.outdoor_lastEvent[cid]["video_status"] == "recording": + for e in self.outdoor_lastEvent[cid]["event_list"]: if e["type"] == "animal" and e["time"] + offset > int(time.time()): return True return False - def carDetected(self, home=None, camera=None, offset=0): + def carDetected(self, home=None, camera=None, offset=0, cid=None): """ Return True if a car has been detected """ - try: - cam_id = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("carDetected: Camera name or home is unknown") - return False + if not cid: + try: + cid = self.cameraByName(camera=camera, home=home)["id"] + except TypeError: + LOG.error("carDetected: Camera name or home is unknown") + return False - if self.outdoor_lastEvent[cam_id]["video_status"] == "recording": - for e in self.outdoor_lastEvent[cam_id]["event_list"]: + if self.outdoor_lastEvent[cid]["video_status"] == "recording": + for e in self.outdoor_lastEvent[cid]["event_list"]: if e["type"] == "vehicle" and e["time"] + offset > int(time.time()): return True return False diff --git a/tests/test_smart_home_camera.py b/tests/test_smart_home_camera.py index 6428fcb48..8cf3d7bb7 100644 --- a/tests/test_smart_home_camera.py +++ b/tests/test_smart_home_camera.py @@ -248,3 +248,18 @@ def test_CameraData_someoneUnknownSeen(cameraHomeData, home, camera, exclude, ex ) def test_CameraData_motionDetected(cameraHomeData, home, camera, exclude, expected): assert cameraHomeData.motionDetected(home, camera, exclude) == expected + + +def test_CameraData_getHomeName(cameraHomeData): + assert cameraHomeData.getHomeName() == "MYHOME" + home_id = "91763b24c43d3e344f424e8b" + assert cameraHomeData.getHomeName(home_id) == "MYHOME" + home_id = "91763b24c43d3e344f424e8c" + assert cameraHomeData.getHomeName(home_id) == "Unknown" + + +def test_CameraData_gethomeId(cameraHomeData): + assert cameraHomeData.gethomeId() == "91763b24c43d3e344f424e8b" + assert cameraHomeData.gethomeId("MYHOME") == "91763b24c43d3e344f424e8b" + with pytest.raises(smart_home.Camera.InvalidHome): + assert cameraHomeData.gethomeId("InvalidName") diff --git a/tests/test_smart_home_homecoach.py b/tests/test_smart_home_homecoach.py index 571a5e674..f3378943d 100644 --- a/tests/test_smart_home_homecoach.py +++ b/tests/test_smart_home_homecoach.py @@ -1,6 +1,10 @@ """Define tests for HomeCoach module.""" +import json + import pytest +import smart_home.HomeCoach + def test_HomeCoachData(homeCoachData): assert homeCoachData.default_station == "Bedroom" @@ -22,3 +26,15 @@ def test_HomeCoachData(homeCoachData): ) def test_HomeCoachData_modulesNamesList(homeCoachData, station, expected): assert sorted(homeCoachData.modulesNamesList(station)) == expected + + +def test_HomeCoachData_no_devices(auth, requests_mock): + with open("fixtures/home_coach_no_devices.json") as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.HomeCoach._GETHOMECOACHDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with pytest.raises(smart_home.WeatherStation.NoDevice): + assert smart_home.HomeCoach.HomeCoachData(auth) diff --git a/tests/test_smart_home_weatherstation.py b/tests/test_smart_home_weatherstation.py index 936936ada..3b81d750f 100644 --- a/tests/test_smart_home_weatherstation.py +++ b/tests/test_smart_home_weatherstation.py @@ -374,3 +374,11 @@ def test_WeatherStationData_getMeasure( weatherStationData.getMeasure(device_id, scale, mtype)["body"]["1544558433"] == expected ) + + +def test_WeatherStationData_lastData_measurements(weatherStationData): + mod = weatherStationData.lastData("MyStation", None) + assert mod["NetatmoIndoor"]["min_temp"] == 23.4 + assert mod["NetatmoIndoor"]["max_temp"] == 25.6 + assert mod["NetatmoIndoor"]["Temperature"] == 24.6 + assert mod["NetatmoIndoor"]["Pressure"] == 1017.3 From dbd85032913e89bddbf1b0bc410dce9b9f07f1ca Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sat, 3 Aug 2019 21:49:55 +0200 Subject: [PATCH 127/333] Sort imports with isort Signed-off-by: Hugo D. (jabesq) --- pyatmo.py | 2 +- smart_home/Camera.py | 2 +- smart_home/Thermostat.py | 2 +- tests/conftest.py | 7 +++---- tests/test_smart_home.py | 1 - tests/test_smart_home_camera.py | 4 +--- tests/test_smart_home_thermostat.py | 6 +++--- tests/test_smart_home_weatherstation.py | 5 ++--- 8 files changed, 12 insertions(+), 17 deletions(-) diff --git a/pyatmo.py b/pyatmo.py index b99606a79..498fd0c1b 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -13,11 +13,11 @@ from smart_home import _BASE_URL, postRequest from smart_home.Camera import CameraData +from smart_home.Exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule from smart_home.HomeCoach import HomeCoachData from smart_home.PublicData import PublicData from smart_home.Thermostat import HomeData, HomeStatus from smart_home.WeatherStation import WeatherStationData -from smart_home.Exceptions import NoDevice, InvalidHome, InvalidRoom, NoSchedule LOG = logging.getLogger(__name__) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index 81908c864..ab4c2c4a8 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -4,7 +4,7 @@ from urllib.error import URLError from . import _BASE_URL, postRequest -from .Exceptions import NoDevice, InvalidHome +from .Exceptions import InvalidHome, NoDevice LOG = logging.getLogger(__name__) diff --git a/smart_home/Thermostat.py b/smart_home/Thermostat.py index c55eb1168..633a618c0 100644 --- a/smart_home/Thermostat.py +++ b/smart_home/Thermostat.py @@ -1,7 +1,7 @@ import logging from . import _BASE_URL, postRequest -from .Exceptions import NoDevice, NoSchedule, InvalidHome, InvalidRoom +from .Exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule LOG = logging.getLogger(__name__) diff --git a/tests/conftest.py b/tests/conftest.py index 434f8cbe9..9cde0c545 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,14 @@ """Define shared fixtures.""" import json +from contextlib import contextmanager import pytest import pyatmo -import smart_home.Thermostat +import smart_home.HomeCoach import smart_home.PublicData +import smart_home.Thermostat import smart_home.WeatherStation -import smart_home.HomeCoach - -from contextlib import contextmanager @contextmanager diff --git a/tests/test_smart_home.py b/tests/test_smart_home.py index 37dff3372..d4f2a321b 100644 --- a/tests/test_smart_home.py +++ b/tests/test_smart_home.py @@ -2,7 +2,6 @@ import time import pytest - import requests import smart_home diff --git a/tests/test_smart_home_camera.py b/tests/test_smart_home_camera.py index 8cf3d7bb7..b033964f8 100644 --- a/tests/test_smart_home_camera.py +++ b/tests/test_smart_home_camera.py @@ -2,13 +2,11 @@ import json import pytest - from freezegun import freeze_time -from .conftest import does_not_raise - import smart_home.Camera +from .conftest import does_not_raise INVALID_NAME = "InvalidName" diff --git a/tests/test_smart_home_thermostat.py b/tests/test_smart_home_thermostat.py index 07ac0fb18..1ce999b7a 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_smart_home_thermostat.py @@ -4,10 +4,10 @@ import pytest -from .conftest import does_not_raise - -import smart_home.Thermostat import smart_home.Exceptions +import smart_home.Thermostat + +from .conftest import does_not_raise def test_HomeData(homeData): diff --git a/tests/test_smart_home_weatherstation.py b/tests/test_smart_home_weatherstation.py index 3b81d750f..08c2fcdf1 100644 --- a/tests/test_smart_home_weatherstation.py +++ b/tests/test_smart_home_weatherstation.py @@ -2,13 +2,12 @@ import json import pytest - from freezegun import freeze_time -from .conftest import does_not_raise - import smart_home.WeatherStation +from .conftest import does_not_raise + def test_WeatherStationData(weatherStationData): assert weatherStationData.default_station == "MyStation" From 0c92fbd348bf230e2684b857f95328c88babed82 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sat, 3 Aug 2019 21:51:12 +0200 Subject: [PATCH 128/333] v2.3.0 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- smart_home/WeatherStation.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 5beb5f5bf..3b14f3a6f 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="2.2.0", # Should be updated with new versions + version="2.3.0", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index ff7cdb3fe..5aa84cec5 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -124,7 +124,13 @@ def monitoredConditions(self, module): conditions.extend(["rf_status", "battery_vp", "battery_percent"]) if mod["type"] in ["NAMain", "NAModule1", "NAModule4", "NHC"]: conditions.extend(["min_temp", "max_temp"]) - if mod["type"] in ["NAMain", "NAModule1", "NAModule2", "NAModule3", "NAModule4"]: + if mod["type"] in [ + "NAMain", + "NAModule1", + "NAModule2", + "NAModule3", + "NAModule4", + ]: conditions.append("reachable") return conditions From 92b01622f627e7e8a7edceba70d0103936f1c944 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Mon, 7 Oct 2019 15:43:41 -0400 Subject: [PATCH 129/333] Bugfix (#79) --- smart_home/WeatherStation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 5aa84cec5..678edadb6 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -154,6 +154,7 @@ def lastData(self, station=None, exclude=0): "time_utc" ) lastD[s["module_name"]]["wifi_status"] = s["wifi_status"] + lastD[s["module_name"]]["reachable"] = s["reachable"] for module in s["modules"]: if "dashboard_data" not in module or "module_name" not in module: continue From bd047f9297aaf65edf043f9f43f79ba0d60845ad Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Mon, 7 Oct 2019 21:44:49 +0200 Subject: [PATCH 130/333] v2.3.1 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3b14f3a6f..2be5cf363 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="2.3.0", # Should be updated with new versions + version="2.3.1", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From 23432d810584262e9ab5fcdb99ddeefc14eff26d Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 14 Oct 2019 20:58:44 +0200 Subject: [PATCH 131/333] Update tests since reachable was added (#80) --- tests/test_smart_home_weatherstation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_smart_home_weatherstation.py b/tests/test_smart_home_weatherstation.py index 08c2fcdf1..884bf7edc 100644 --- a/tests/test_smart_home_weatherstation.py +++ b/tests/test_smart_home_weatherstation.py @@ -148,6 +148,7 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): "humidity", "max_temp", "min_temp", + "reachable", "rf_status", "temperature", ], @@ -159,6 +160,7 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): "battery_vp", "gustangle", "guststrength", + "reachable", "rf_status", "windangle", "windstrength", @@ -170,6 +172,7 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): "Rain", "battery_percent", "battery_vp", + "reachable", "rf_status", "sum_rain_1", "sum_rain_24", @@ -184,6 +187,7 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): "min_temp", "noise", "pressure", + "reachable", "temperature", "wifi_status", ], From 91e6c11dd7f6f67b11b903324f172902f95713c5 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 14 Oct 2019 20:59:09 +0200 Subject: [PATCH 132/333] Fix missing dependencies (#82) * Fix missing dependencies * Add github action * Revert "Add github action" This reverts commit 98835320c08417df9c7db4defe99d67b6f2af82c. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 2be5cf363..145875e5d 100755 --- a/setup.py +++ b/setup.py @@ -21,4 +21,5 @@ "Design for Home-Assitant (but not only)" ), long_description=open("README.md").read(), + install_requires=["requests"], ) From 96a787fcf33bc7c1638ea0249fdadf69ccf8e480 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Mon, 14 Oct 2019 21:02:31 +0200 Subject: [PATCH 133/333] v2.3.2 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 145875e5d..15b03eba5 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="2.3.1", # Should be updated with new versions + version="2.3.2", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From 8ea8d8b2d809b0eec63d5fae554c492e0ba82728 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Mon, 14 Oct 2019 21:05:53 +0200 Subject: [PATCH 134/333] Update pipfile to use setup.py data Signed-off-by: Hugo D. (jabesq) --- Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile b/Pipfile index 9605f3ed7..b9ee2fda7 100644 --- a/Pipfile +++ b/Pipfile @@ -17,6 +17,7 @@ requests-mock = "*" freezegun = "*" [packages] +pyatmo = {editable = true,path = "."} [requires] python_version = "3.7" From e762857fefe7cf84316da38ecd5bb5a878e5834f Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 21 Oct 2019 16:50:47 +0200 Subject: [PATCH 135/333] Add GitHub Actions (#83) * Fix missing dependencies * Add github action * Update pythonpackage.yml * Update pythonpackage.yml * Blacken code * Update pythonpackage.yml * Update pythonpackage.yml * Add github action * Fix timestamp conversion * Add homecoach test fixture --- .github/workflows/pythonpackage.yml | 44 +++++++++++++++++++++++++++++ fixtures/home_coach_no_devices.json | 20 +++++++++++++ smart_home/PublicData.py | 8 +----- smart_home/__init__.py | 9 +++--- tests/test_smart_home.py | 22 +++++++-------- tests/test_smart_home_publicdata.py | 8 ------ 6 files changed, 81 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/pythonpackage.yml create mode 100644 fixtures/home_coach_no_devices.json diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml new file mode 100644 index 000000000..b90bf7b03 --- /dev/null +++ b/.github/workflows/pythonpackage.yml @@ -0,0 +1,44 @@ +name: Python package + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: [3.6, 3.7] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + - name: Lint with flake8 + run: | + pip install flake8 + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Check with black + run: | + pip install black + black --check smart_home/ tests/ setup.py + - name: Test with pytest + run: | + pip install pytest freezegun requests-mock + pytest diff --git a/fixtures/home_coach_no_devices.json b/fixtures/home_coach_no_devices.json new file mode 100644 index 000000000..95d0181f8 --- /dev/null +++ b/fixtures/home_coach_no_devices.json @@ -0,0 +1,20 @@ +{ + "body": { + "devices": [], + "user": { + "mail": "john@doe.com", + "administrative": { + "lang": "de-DE", + "reg_locale": "de-DE", + "country": "DE", + "unit": 0, + "windunit": 0, + "pressureunit": 0, + "feel_like_algo": 0 + } + } + }, + "status": "ok", + "time_exec": 0.05824708938598633, + "time_server": 1565377059 +} \ No newline at end of file diff --git a/smart_home/PublicData.py b/smart_home/PublicData.py index aaae7d3f4..3527274e4 100644 --- a/smart_home/PublicData.py +++ b/smart_home/PublicData.py @@ -1,6 +1,4 @@ -import time - -from . import _BASE_URL, postRequest +from . import _BASE_URL, postRequest, toTimeString from .Exceptions import NoDevice _GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata" @@ -168,10 +166,6 @@ def getAccessoryMeasures(self, type): return measures -def toTimeString(value): - return time.strftime("%Y-%m-%d_%H:%M:%S", time.localtime(int(value))) - - def averageMeasure(measures): if measures: return sum(measures.values()) / len(measures) diff --git a/smart_home/__init__.py b/smart_home/__init__.py index de79695c0..41f25b222 100644 --- a/smart_home/__init__.py +++ b/smart_home/__init__.py @@ -1,5 +1,7 @@ import logging import time +from calendar import timegm +from datetime import datetime import requests @@ -28,16 +30,15 @@ def postRequest(url, params=None, timeout=10): def toTimeString(value): - return time.strftime("%Y-%m-%d_%H:%M:%S", time.localtime(int(value))) + return datetime.utcfromtimestamp(int(value)).isoformat(sep="_") def toEpoch(value): - return int(time.mktime(time.strptime(value, "%Y-%m-%d_%H:%M:%S"))) + return timegm(time.strptime(value + "GMT", "%Y-%m-%d_%H:%M:%S%Z")) def todayStamps(): - today = time.strftime("%Y-%m-%d") - today = int(time.mktime(time.strptime(today, "%Y-%m-%d"))) + today = timegm(time.strptime(time.strftime("%Y-%m-%d") + "GMT", "%Y-%m-%d%Z")) return today, today + 3600 * 24 diff --git a/tests/test_smart_home.py b/tests/test_smart_home.py index d4f2a321b..1643e32b8 100644 --- a/tests/test_smart_home.py +++ b/tests/test_smart_home.py @@ -47,11 +47,11 @@ def test_postRequest_timeout(requests_mock): @pytest.mark.parametrize( "test_input,expected", [ - (1, "1970-01-01_01:00:01"), - (0, "1970-01-01_01:00:00"), - (-1, "1970-01-01_00:59:59"), - (2000000000, "2033-05-18_05:33:20"), - ("1", "1970-01-01_01:00:01"), + (1, "1970-01-01_00:00:01"), + (0, "1970-01-01_00:00:00"), + (-1, "1969-12-31_23:59:59"), + (2000000000, "2033-05-18_03:33:20"), + ("1", "1970-01-01_00:00:01"), pytest.param("A", None, marks=pytest.mark.xfail), pytest.param([1], None, marks=pytest.mark.xfail), pytest.param({1}, None, marks=pytest.mark.xfail), @@ -65,10 +65,10 @@ def test_toTimeString(test_input, expected): @pytest.mark.parametrize( "test_input,expected", [ - ("1970-01-01_01:00:01", 1), - ("1970-01-01_01:00:00", 0), - ("1970-01-01_00:59:59", -1), - ("2033-05-18_05:33:20", 2000000000), + ("1970-01-01_00:00:01", 1), + ("1970-01-01_00:00:00", 0), + ("1969-12-31_23:59:59", -1), + ("2033-05-18_03:33:20", 2000000000), ], ) def test_toEpoch(test_input, expected): @@ -79,8 +79,8 @@ def test_toEpoch(test_input, expected): @pytest.mark.parametrize( "test_input,expected", [ - ("2018-06-21", (1529532000, 1529618400)), - ("2000-01-01", (946681200, 946767600)), + ("2018-06-21", (1529539200, 1529625600)), + ("2000-01-01", (946684800, 946771200)), pytest.param("2000-04-31", None, marks=pytest.mark.xfail), ], ) diff --git a/tests/test_smart_home_publicdata.py b/tests/test_smart_home_publicdata.py index da32032ec..d19627623 100644 --- a/tests/test_smart_home_publicdata.py +++ b/tests/test_smart_home_publicdata.py @@ -264,14 +264,6 @@ def test_PublicData_getAccessoryMeasures(publicData, test_input, expected): assert publicData.getAccessoryMeasures(test_input) == expected -@pytest.mark.parametrize( - "test_input,expected", - [(1560248190, "2019-06-11_12:16:30"), (0, "1970-01-01_01:00:00")], -) -def test_PublicData_toTimeString(test_input, expected): - assert smart_home.PublicData.toTimeString(test_input) == expected - - @pytest.mark.parametrize( "test_input,expected", [ From 774d15cc5fdbe4cb89ad70383f8cb7b7fb342160 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 21 Oct 2019 16:51:16 +0200 Subject: [PATCH 136/333] Add reachable attribute for Home Coach devices (#84) --- fixtures/home_coach_simple.json | 4 ++-- smart_home/WeatherStation.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fixtures/home_coach_simple.json b/fixtures/home_coach_simple.json index 08c1c1e05..88ea883ca 100644 --- a/fixtures/home_coach_simple.json +++ b/fixtures/home_coach_simple.json @@ -42,7 +42,7 @@ "firmware": 45, "last_upgrade": 1544562194, "wifi_status": 41, - "reachable": false, + "reachable": true, "co2_calibrating": false, "station_name": "Kitchen", "data_type": [ @@ -73,7 +73,7 @@ "firmware": 45, "last_upgrade": 1544564302, "wifi_status": 66, - "reachable": false, + "reachable": true, "co2_calibrating": false, "station_name": "Livingroom", "data_type": [ diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 678edadb6..0e82ea11f 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -130,6 +130,7 @@ def monitoredConditions(self, module): "NAModule2", "NAModule3", "NAModule4", + "NHC", ]: conditions.append("reachable") return conditions From 3b13e66d7650cf7112f02fa0e035dbbe6bb8ef48 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 21 Oct 2019 22:55:21 +0200 Subject: [PATCH 137/333] Add some badges --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ca28b362a..675b599b2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ netatmo-api-python ================== [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) +[![GitHub Actions](https://github.com/jabesq/netatmo-api-python/workflows/Python%20package/badge.svg)](https://github.com/jabesq/netatmo-api-python/actions?workflow=Python+package) +[![PyPi](https://img.shields.io/pypi/v/pyatmo.svg)](https://pypi.python.org/pypi/pyatmo) +[![PyPi](https://img.shields.io/pypi/l/pyatmo.svg)](https://github.com/jabesq/netatmo-api-python/blob/master/LICENSE.txt) + Simple API to access Netatmo devices and data like weather station or camera data from Python 3. For more detailed information see http://dev.netatmo.com @@ -40,4 +44,4 @@ or To generate code coverage xml (e.g. for use in VSCode) run - python -m pytest --cov-report xml:cov.xml --cov smart_home --cov-append tests/ \ No newline at end of file + python -m pytest --cov-report xml:cov.xml --cov smart_home --cov-append tests/ From 840d4a89505b0522a41b1ce81e68a83264df24ef Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 30 Oct 2019 19:12:32 +0100 Subject: [PATCH 138/333] Rephrase some docstrings and fix linting errors (#86) --- smart_home/Camera.py | 32 ++++++++++++++++---------------- smart_home/WeatherStation.py | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/smart_home/Camera.py b/smart_home/Camera.py index ab4c2c4a8..bf8e2bea6 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -15,7 +15,7 @@ class CameraData: """ - List the Netatmo cameras informations + List the Netatmo camera informations (Homes, cameras, modules, events, persons) Args: authData (ClientAuth): @@ -125,7 +125,7 @@ def gethomeId(self, home=None): raise InvalidHome("Invalid Home %s" % home) def cameraById(self, cid): - for home, cam in self.cameras.items(): + for home, _ in self.cameras.items(): if cid in self.cameras[home]: return self.cameras[home][cid] return None @@ -196,7 +196,7 @@ def cameraType(self, camera=None, home=None, cid=None, home_id=None): def cameraUrls(self, camera=None, home=None, cid=None, home_id=None): """ Return the vpn_url and the local_url (if available) of a given camera - in order to access to its live feed + in order to access its live feed """ local_url = None vpn_url = None @@ -266,7 +266,7 @@ def getProfileImage(self, name): def updateEvent(self, event=None, home=None, cameratype=None, home_id=None): """ - Update the list of event with the latest ones + Update the list of events """ if not home_id: try: @@ -322,7 +322,7 @@ def updateEvent(self, event=None, home=None, cameratype=None, home_id=None): def personSeenByCamera(self, name, home=None, camera=None, exclude=0): """ - Return True if a specific person has been seen by a camera + Evaluate if a specific person has been seen """ try: cam_id = self.cameraByName(camera=camera, home=home)["id"] @@ -357,13 +357,13 @@ def _knownPersons(self): def knownPersonsNames(self): names = [] - for p_id, p in self._knownPersons().items(): + for _, p in self._knownPersons().items(): names.append(p["pseudo"]) return names def someoneKnownSeen(self, home=None, camera=None, exclude=0, cid=None): """ - Return True if someone known has been seen + Evaluate if someone known has been seen """ if not cid: try: @@ -381,7 +381,7 @@ def someoneKnownSeen(self, home=None, camera=None, exclude=0, cid=None): elif self.events[cid][time_ev]["type"] == "person": if self.events[cid][time_ev]["person_id"] in self._knownPersons(): return True - # Check in the last event is someone known has been seen + # Check in the last event if someone known has been seen elif self.lastEvent[cid]["type"] == "person": if self.lastEvent[cid]["person_id"] in self._knownPersons(): return True @@ -389,7 +389,7 @@ def someoneKnownSeen(self, home=None, camera=None, exclude=0, cid=None): def someoneUnknownSeen(self, home=None, camera=None, exclude=0, cid=None): """ - Return True if someone unknown has been seen + Evaluate if someone unknown has been seen """ if not cid: try: @@ -418,7 +418,7 @@ def someoneUnknownSeen(self, home=None, camera=None, exclude=0, cid=None): def motionDetected(self, home=None, camera=None, exclude=0, cid=None): """ - Return True if movement has been detected + Evaluate if movement has been detected """ if not cid: try: @@ -441,7 +441,7 @@ def motionDetected(self, home=None, camera=None, exclude=0, cid=None): def outdoormotionDetected(self, home=None, camera=None, offset=0, cid=None): """ - Return True if outdoor movement has been detected + Evaluate if outdoor movement has been detected """ if not cid: try: @@ -462,7 +462,7 @@ def outdoormotionDetected(self, home=None, camera=None, offset=0, cid=None): def humanDetected(self, home=None, camera=None, offset=0, cid=None): """ - Return True if a human has been detected + Evaluate if a human has been detected """ if not cid: try: @@ -479,7 +479,7 @@ def humanDetected(self, home=None, camera=None, offset=0, cid=None): def animalDetected(self, home=None, camera=None, offset=0, cid=None): """ - Return True if an animal has been detected + Evaluate if an animal has been detected """ if not cid: try: @@ -496,7 +496,7 @@ def animalDetected(self, home=None, camera=None, offset=0, cid=None): def carDetected(self, home=None, camera=None, offset=0, cid=None): """ - Return True if a car has been detected + Evaluate if a car has been detected """ if not cid: try: @@ -513,7 +513,7 @@ def carDetected(self, home=None, camera=None, offset=0, cid=None): def moduleMotionDetected(self, module=None, home=None, camera=None, exclude=0): """ - Return True if movement has been detected + Evaluate if movement has been detected """ try: mod = self.moduleByName(module, camera=camera, home=home) @@ -545,7 +545,7 @@ def moduleMotionDetected(self, module=None, home=None, camera=None, exclude=0): def moduleOpened(self, module=None, home=None, camera=None, exclude=0): """ - Return True if module status is open + Evaluate if module status is open """ try: mod = self.moduleByName(module, camera=camera, home=home) diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 0e82ea11f..1b198aa44 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -59,7 +59,7 @@ def modulesNamesList(self, station=None, station_id=None): res.add(m["module_name"]) else: res.update([m["module_name"] for m in self.modules.values()]) - for id, station in self.stations.items(): + for _, station in self.stations.items(): res.add(station["module_name"]) return list(res) @@ -83,7 +83,7 @@ def moduleByName(self, module, station=None): elif s["module_name"] == module: return s else: - for id, station in self.stations.items(): + for _, station in self.stations.items(): if "module_name" in station and station["module_name"] == module: return station for m in self.modules: From 164d34cb5859f9781f229bed6322d16559619be8 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 30 Oct 2019 19:14:21 +0100 Subject: [PATCH 139/333] Add setpersonhome/away services (#85) * Add setpersonhome/away services * Blacken code --- pyatmo.py | 7 +-- smart_home/Camera.py | 55 +++++++++++++++++++++--- tests/test_smart_home_camera.py | 75 +++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 9 deletions(-) diff --git a/pyatmo.py b/pyatmo.py index 498fd0c1b..35597f43f 100644 --- a/pyatmo.py +++ b/pyatmo.py @@ -36,15 +36,16 @@ class ClientAuth: clientSecret (str): Application Secret key delivered by Netatmo on dev.netatmo.com username (str) password (str) - scope (Optional[str]): Default value is 'read_station' + scope (Optional[str]): read_station: to retrieve weather station data (Getstationsdata, Getmeasure) read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture) access_camera: to access the camera, the videos and the live stream. + write_camera: to set home/away status of persons (Setpersonsaway, Setpersonshome) read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata) write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint) read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) - read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status + read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) Several value can be used at the same time, ie: 'read_station read_camera' """ @@ -132,7 +133,7 @@ def accessToken(self): clientSecret=CLIENT_SECRET, username=USERNAME, password=PASSWORD, - scope="read_station read_camera access_camera read_thermostat write_thermostat read_presence access_presence", + scope="read_station read_camera access_camera write_camera read_thermostat write_thermostat read_presence access_presence read_homecoach", ) try: diff --git a/smart_home/Camera.py b/smart_home/Camera.py index bf8e2bea6..66f3c3641 100644 --- a/smart_home/Camera.py +++ b/smart_home/Camera.py @@ -11,6 +11,8 @@ _GETHOMEDATA_REQ = _BASE_URL + "api/gethomedata" _GETCAMERAPICTURE_REQ = _BASE_URL + "api/getcamerapicture" _GETEVENTSUNTIL_REQ = _BASE_URL + "api/geteventsuntil" +_SETPERSONSAWAY_REQ = _BASE_URL + "api/setpersonsaway" +_SETPERSONSHOME_REQ = _BASE_URL + "api/setpersonshome" class CameraData: @@ -92,7 +94,10 @@ def __init__(self, authData, size=15): self.default_module = list(self.modules.values())[0]["name"] else: self.default_module = None - if self.default_home is not None and len(self.cameras) > 0: + if ( + self.default_home is not None + and len(self.cameras[self.default_home_id]) > 0 + ): self.default_camera = list(self.cameras[self.default_home_id].values())[0] def homeById(self, hid): @@ -224,21 +229,56 @@ def cameraUrls(self, camera=None, home=None, cid=None, home_id=None): pass return vpn_url, local_url - def personsAtHome(self, home=None): + def personsAtHome(self, home=None, home_id=None): """ Return the list of known persons who are currently at home """ - if not home: - home = self.default_home - home_data = self.homeByName(home) + if home_id: + home_data = self.homeById(home_id) + else: + if not home: + home = self.default_home + home_data = self.homeByName(home) atHome = [] for p in home_data["persons"]: - # Only check known persons + # Only check known personshome if "pseudo" in p: if not p["out_of_sight"]: atHome.append(p["pseudo"]) return atHome + def setPersonsHome(self, person_ids, home_id): + """ + Mark persons as home. + """ + postParams = { + "access_token": self.getAuthToken, + "home_id": home_id, + "person_ids[]": person_ids, + } + resp = postRequest(_SETPERSONSHOME_REQ, postParams) + LOG.debug(resp) + return resp + + def setPersonsAway(self, person_id, home_id): + """ + Mark a person as away or set the whole home to being empty. + """ + postParams = { + "access_token": self.getAuthToken, + "home_id": home_id, + "person_id": person_id, + } + resp = postRequest(_SETPERSONSAWAY_REQ, postParams) + LOG.debug(resp) + return resp + + def getPersonId(self, name): + for pid, data in self.persons.items(): + if "pseudo" in data and name == data["pseudo"]: + return pid + return None + def getCameraPicture(self, image_id, key): """ Download a specific image (of an event or user face) from the camera @@ -355,6 +395,9 @@ def _knownPersons(self): known_persons[p_id] = p return known_persons + def knownPersons(self): + return {pid: p["pseudo"] for pid, p in self._knownPersons().items()} + def knownPersonsNames(self): names = [] for _, p in self._knownPersons().items(): diff --git a/tests/test_smart_home_camera.py b/tests/test_smart_home_camera.py index b033964f8..79d45e52f 100644 --- a/tests/test_smart_home_camera.py +++ b/tests/test_smart_home_camera.py @@ -205,6 +205,81 @@ def test_CameraData_knownPersonsNames(cameraHomeData): ] +@freeze_time("2019-06-16") +@pytest.mark.parametrize( + "name, expected", + [ + ("John Doe", "91827374-7e04-5298-83ad-a0cb8372dff1"), + ("Richard Doe", "91827376-7e04-5298-83af-a0cb8372dff3"), + ], +) +def test_CameraData_getPersonId(cameraHomeData, name, expected): + assert cameraHomeData.getPersonId(name) == expected + + +@pytest.mark.parametrize( + "hid, pid, json_fixture, expected", + [ + ( + "91763b24c43d3e344f424e8b", + "91827374-7e04-5298-83ad-a0cb8372dff1", + "status_ok.json", + "ok", + ), + ( + "91763b24c43d3e344f424e8b", + "91827376-7e04-5298-83af-a0cb8372dff3", + "status_ok.json", + "ok", + ), + ], +) +def test_CameraData_setPersonsAway( + cameraHomeData, requests_mock, hid, pid, json_fixture, expected +): + with open("fixtures/%s" % json_fixture) as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Camera._SETPERSONSAWAY_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + assert cameraHomeData.setPersonsAway(pid, hid)["status"] == expected + + +@pytest.mark.parametrize( + "hid, pids, json_fixture, expected", + [ + ( + "91763b24c43d3e344f424e8b", + [ + "91827374-7e04-5298-83ad-a0cb8372dff1", + "91827376-7e04-5298-83af-a0cb8372dff3", + ], + "status_ok.json", + "ok", + ), + ( + "91763b24c43d3e344f424e8b", + "91827376-7e04-5298-83af-a0cb8372dff3", + "status_ok.json", + "ok", + ), + ], +) +def test_CameraData_setPersonsHome( + cameraHomeData, requests_mock, hid, pids, json_fixture, expected +): + with open("fixtures/%s" % json_fixture) as f: + json_fixture = json.load(f) + requests_mock.post( + smart_home.Camera._SETPERSONSHOME_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + assert cameraHomeData.setPersonsHome(pids, hid)["status"] == expected + + @freeze_time("2019-06-16") @pytest.mark.parametrize( "home, camera, exclude, expected", From 90757f63a09bf71348d2aaaa1e2edaac89f8047f Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 6 Nov 2019 16:51:52 +0100 Subject: [PATCH 140/333] Fix issue with multiple homecoach devices (#88) * Add getModules method * Dict key can be either module_name or id * Blacken code --- fixtures/home_coach_simple.json | 88 ++++++++++++++ smart_home/WeatherStation.py | 66 ++++++++--- tests/test_smart_home_homecoach.py | 58 ++++++++- tests/test_smart_home_weatherstation.py | 150 ++++++++++++++++++++++++ 4 files changed, 345 insertions(+), 17 deletions(-) diff --git a/fixtures/home_coach_simple.json b/fixtures/home_coach_simple.json index 88ea883ca..3f9de74bd 100644 --- a/fixtures/home_coach_simple.json +++ b/fixtures/home_coach_simple.json @@ -93,6 +93,94 @@ 13.377726 ] } + }, + { + "_id": "12:34:56:3e:c5:46", + "station_name": "Parents Bedroom", + "date_setup": 1570732241, + "last_setup": 1570732241, + "type": "NHC", + "last_status_store": 1572073818, + "module_name": "Indoor", + "firmware": 45, + "wifi_status": 67, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure", + "health_idx" + ], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [ + 52.516263, + 13.377726 + ] + }, + "dashboard_data": { + "time_utc": 1572073816, + "Temperature": 20.3, + "CO2": 494, + "Humidity": 63, + "Noise": 42, + "Pressure": 1014.5, + "AbsolutePressure": 1004.1, + "health_idx": 1, + "min_temp": 20.3, + "max_temp": 21.6, + "date_max_temp": 1572059333, + "date_min_temp": 1572073816 + } + }, + { + "_id": "12:34:56:26:68:92", + "station_name": "Baby Bedroom", + "date_setup": 1571342643, + "last_setup": 1571342643, + "type": "NHC", + "last_status_store": 1572073995, + "module_name": "Indoor", + "firmware": 45, + "wifi_status": 68, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure", + "health_idx" + ], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [ + 52.516263, + 13.377726 + ] + }, + "dashboard_data": { + "time_utc": 1572073994, + "Temperature": 21.6, + "CO2": 1053, + "Humidity": 66, + "Noise": 45, + "Pressure": 1021.4, + "AbsolutePressure": 1011, + "health_idx": 1, + "min_temp": 20.9, + "max_temp": 21.6, + "date_max_temp": 1572073690, + "date_min_temp": 1572064254 + } } ], "user": { diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py index 1b198aa44..f3f6ac02c 100644 --- a/smart_home/WeatherStation.py +++ b/smart_home/WeatherStation.py @@ -63,6 +63,33 @@ def modulesNamesList(self, station=None, station_id=None): res.add(station["module_name"]) return list(res) + def getModules(self, station=None, station_id=None): + """Return a dict for Home Assistant to consume.""" + res = {} + station_data = None + if station_id is not None: + station_data = self.stationById(station_id) + elif station is not None: + station_data = self.stationByName(station) + if station_data is not None: + stations = [self.stations[station_data["_id"]]] + else: + stations = self.stations.values() + for s in stations: + res[s["_id"]] = { + "station_name": s["station_name"], + "module_name": s["module_name"], + "id": s["_id"], + } + + for m in s["modules"]: + res[m["_id"]] = { + "station_name": m.get("station_name", s["station_name"]), + "module_name": m["module_name"], + "id": m["_id"], + } + return res + def stationByName(self, station=None): if not station: station = self.default_station @@ -103,10 +130,17 @@ def moduleById(self, mid, sid=None): else: return self.modules[mid] - def monitoredConditions(self, module): - mod = self.moduleByName(module) + def monitoredConditions(self, module=None, moduleId=None): + if moduleId: + mod = self.moduleById(moduleId) + elif module: + mod = self.moduleByName(module) + else: + return None conditions = [] - for cond in mod["data_type"]: + if not mod: + return conditions + for cond in mod.get("data_type", []): if cond == "Wind": # the Wind meter actually exposes the following conditions conditions.extend( @@ -135,7 +169,11 @@ def monitoredConditions(self, module): conditions.append("reachable") return conditions - def lastData(self, station=None, exclude=0): + def lastData(self, station=None, exclude=0, byId=False): + if byId: + key = "_id" + else: + key = "module_name" if station is not None: stations = [station] else: @@ -150,21 +188,17 @@ def lastData(self, station=None, exclude=0): limit = (time.time() - exclude) if exclude else 0 ds = s["dashboard_data"] if "module_name" in s and ds["time_utc"] > limit: - lastD[s["module_name"]] = ds.copy() - lastD[s["module_name"]]["When"] = lastD[s["module_name"]].pop( - "time_utc" - ) - lastD[s["module_name"]]["wifi_status"] = s["wifi_status"] - lastD[s["module_name"]]["reachable"] = s["reachable"] + lastD[s[key]] = ds.copy() + lastD[s[key]]["When"] = lastD[s[key]].pop("time_utc") + lastD[s[key]]["wifi_status"] = s["wifi_status"] + lastD[s[key]]["reachable"] = s["reachable"] for module in s["modules"]: - if "dashboard_data" not in module or "module_name" not in module: + if "dashboard_data" not in module or key not in module: continue ds = module["dashboard_data"] if "time_utc" in ds and ds["time_utc"] > limit: - lastD[module["module_name"]] = ds.copy() - lastD[module["module_name"]]["When"] = lastD[ - module["module_name"] - ].pop("time_utc") + lastD[module[key]] = ds.copy() + lastD[module[key]]["When"] = lastD[module[key]].pop("time_utc") # For potential use, add battery and radio coverage information to module data if present for i in ( "rf_status", @@ -174,7 +208,7 @@ def lastData(self, station=None, exclude=0): "wifi_status", ): if i in module: - lastD[module["module_name"]][i] = module[i] + lastD[module[key]][i] = module[i] return lastD def checkNotUpdated(self, station=None, delay=3600): diff --git a/tests/test_smart_home_homecoach.py b/tests/test_smart_home_homecoach.py index f3378943d..3fad787cb 100644 --- a/tests/test_smart_home_homecoach.py +++ b/tests/test_smart_home_homecoach.py @@ -13,7 +13,7 @@ def test_HomeCoachData(homeCoachData): @pytest.mark.parametrize( "station, expected", [ - (None, ["Bedroom", "Kitchen", "Livingroom"]), + (None, ["Bedroom", "Indoor", "Kitchen", "Livingroom"]), ("Bedroom", ["Bedroom"]), pytest.param( "NoValidStation", @@ -28,6 +28,62 @@ def test_HomeCoachData_modulesNamesList(homeCoachData, station, expected): assert sorted(homeCoachData.modulesNamesList(station)) == expected +@pytest.mark.parametrize( + "station, expected", + [ + ( + None, + { + "12:34:56:26:69:0c": { + "station_name": "Bedroom", + "module_name": "Bedroom", + "id": "12:34:56:26:69:0c", + }, + "12:34:56:25:cf:a8": { + "station_name": "Kitchen", + "module_name": "Kitchen", + "id": "12:34:56:25:cf:a8", + }, + "12:34:56:26:65:14": { + "station_name": "Livingroom", + "module_name": "Livingroom", + "id": "12:34:56:26:65:14", + }, + "12:34:56:3e:c5:46": { + "station_name": "Parents Bedroom", + "module_name": "Indoor", + "id": "12:34:56:3e:c5:46", + }, + "12:34:56:26:68:92": { + "station_name": "Baby Bedroom", + "module_name": "Indoor", + "id": "12:34:56:26:68:92", + }, + }, + ), + ( + "Bedroom", + { + "12:34:56:26:69:0c": { + "station_name": "Bedroom", + "module_name": "Bedroom", + "id": "12:34:56:26:69:0c", + } + }, + ), + pytest.param( + "NoValidStation", + None, + marks=pytest.mark.xfail( + reason="Invalid station names are not handled yet." + ), + ), + ], +) +def test_HomeCoachData_getModules(homeCoachData, station, expected): + assert homeCoachData.getModules(station) == expected + + def test_HomeCoachData_no_devices(auth, requests_mock): with open("fixtures/home_coach_no_devices.json") as f: json_fixture = json.load(f) diff --git a/tests/test_smart_home_weatherstation.py b/tests/test_smart_home_weatherstation.py index 884bf7edc..e593e747a 100644 --- a/tests/test_smart_home_weatherstation.py +++ b/tests/test_smart_home_weatherstation.py @@ -81,6 +81,92 @@ def test_WeatherStationData_modulesNamesList(weatherStationData, station, expect assert sorted(weatherStationData.modulesNamesList(station)) == expected +@pytest.mark.parametrize( + "station, expected", + [ + ( + None, + { + "12:34:56:03:1b:e4": { + "id": "12:34:56:03:1b:e4", + "module_name": "Garden", + "station_name": "MyStation", + }, + "12:34:56:05:51:20": { + "id": "12:34:56:05:51:20", + "module_name": "Yard", + "station_name": "MyStation", + }, + "12:34:56:07:bb:0e": { + "id": "12:34:56:07:bb:0e", + "module_name": "Livingroom", + "station_name": "MyStation", + }, + "12:34:56:07:bb:3e": { + "id": "12:34:56:07:bb:3e", + "module_name": "Kitchen", + "station_name": "MyStation", + }, + "12:34:56:36:fc:de": { + "id": "12:34:56:36:fc:de", + "module_name": "NetatmoOutdoor", + "station_name": "MyStation", + }, + "12:34:56:37:11:ca": { + "id": "12:34:56:37:11:ca", + "module_name": "NetatmoIndoor", + "station_name": "MyStation", + }, + }, + ), + ( + "MyStation", + { + "12:34:56:03:1b:e4": { + "id": "12:34:56:03:1b:e4", + "module_name": "Garden", + "station_name": "MyStation", + }, + "12:34:56:05:51:20": { + "id": "12:34:56:05:51:20", + "module_name": "Yard", + "station_name": "MyStation", + }, + "12:34:56:07:bb:0e": { + "id": "12:34:56:07:bb:0e", + "module_name": "Livingroom", + "station_name": "MyStation", + }, + "12:34:56:07:bb:3e": { + "id": "12:34:56:07:bb:3e", + "module_name": "Kitchen", + "station_name": "MyStation", + }, + "12:34:56:36:fc:de": { + "id": "12:34:56:36:fc:de", + "module_name": "NetatmoOutdoor", + "station_name": "MyStation", + }, + "12:34:56:37:11:ca": { + "id": "12:34:56:37:11:ca", + "module_name": "NetatmoIndoor", + "station_name": "MyStation", + }, + }, + ), + pytest.param( + "NoValidStation", + None, + marks=pytest.mark.xfail( + reason="Invalid station names are not handled yet." + ), + ), + ], +) +def test_WeatherStationData_getModules(weatherStationData, station, expected): + assert weatherStationData.getModules(station) == expected + + def test_WeatherStationData_stationByName(weatherStationData): result = weatherStationData.stationByName() assert result["_id"] == "12:34:56:37:11:ca" @@ -269,6 +355,70 @@ def test_WeatherStationData_lastData(weatherStationData, station, exclude, expec assert mod is expected +@freeze_time("2019-06-11") +@pytest.mark.parametrize( + "station, exclude, expected", + [ + ( + "MyStation", + None, + [ + "12:34:56:03:1b:e4", + "12:34:56:05:51:20", + "12:34:56:07:bb:0e", + "12:34:56:07:bb:3e", + "12:34:56:36:fc:de", + "12:34:56:37:11:ca", + ], + ), + ( + "", + None, + [ + "12:34:56:03:1b:e4", + "12:34:56:05:51:20", + "12:34:56:07:bb:0e", + "12:34:56:07:bb:3e", + "12:34:56:36:fc:de", + "12:34:56:37:11:ca", + ], + ), + ("NoValidStation", None, None), + ( + None, + 1000000, + [ + "12:34:56:03:1b:e4", + "12:34:56:05:51:20", + "12:34:56:07:bb:0e", + "12:34:56:07:bb:3e", + "12:34:56:36:fc:de", + "12:34:56:37:11:ca", + ], + ), + ( + None, + 798103, + [ + "12:34:56:03:1b:e4", + "12:34:56:05:51:20", + "12:34:56:07:bb:3e", + "12:34:56:36:fc:de", + "12:34:56:37:11:ca", + ], + ), + ], +) +def test_WeatherStationData_lastData_byId( + weatherStationData, station, exclude, expected +): + mod = weatherStationData.lastData(station, exclude, byId=True) + if mod: + assert sorted(mod) == expected + else: + assert mod is expected + + @freeze_time("2019-06-11") @pytest.mark.parametrize( "station, delay, expected", From 5a1b065bd5fad23f4dcb8b208848aa853afb064b Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 29 Oct 2019 00:42:14 +0100 Subject: [PATCH 141/333] Refactor package --- pyatmo.py => pyatmo/Auth.py | 84 +--------------- {smart_home => pyatmo}/Camera.py | 6 +- {smart_home => pyatmo}/Exceptions.py | 0 {smart_home => pyatmo}/HomeCoach.py | 4 +- {smart_home => pyatmo}/PublicData.py | 4 +- {smart_home => pyatmo}/Thermostat.py | 4 +- {smart_home => pyatmo}/WeatherStation.py | 4 +- {smart_home => pyatmo}/__init__.py | 0 pyatmo/__main__.py | 69 +++++++++++++ tests/conftest.py | 39 ++++---- tests/test_pyatmo.py | 96 ++++++++++++++++++- ...t_home_camera.py => test_pyatmo_camera.py} | 13 ++- ..._homecoach.py => test_pyatmo_homecoach.py} | 8 +- ...ublicdata.py => test_pyatmo_publicdata.py} | 22 ++--- ...hermostat.py => test_pyatmo_thermostat.py} | 68 ++++++------- ...ation.py => test_pyatmo_weatherstation.py} | 22 ++--- tests/test_smart_home.py | 94 ------------------ 17 files changed, 259 insertions(+), 278 deletions(-) rename pyatmo.py => pyatmo/Auth.py (58%) rename {smart_home => pyatmo}/Camera.py (99%) rename {smart_home => pyatmo}/Exceptions.py (100%) rename {smart_home => pyatmo}/HomeCoach.py (82%) rename {smart_home => pyatmo}/PublicData.py (98%) rename {smart_home => pyatmo}/Thermostat.py (99%) rename {smart_home => pyatmo}/WeatherStation.py (99%) rename {smart_home => pyatmo}/__init__.py (100%) create mode 100644 pyatmo/__main__.py rename tests/{test_smart_home_camera.py => test_pyatmo_camera.py} (96%) rename tests/{test_smart_home_homecoach.py => test_pyatmo_homecoach.py} (93%) rename tests/{test_smart_home_publicdata.py => test_pyatmo_publicdata.py} (93%) rename tests/{test_smart_home_thermostat.py => test_pyatmo_thermostat.py} (87%) rename tests/{test_smart_home_weatherstation.py => test_pyatmo_weatherstation.py} (95%) delete mode 100644 tests/test_smart_home.py diff --git a/pyatmo.py b/pyatmo/Auth.py similarity index 58% rename from pyatmo.py rename to pyatmo/Auth.py index 35597f43f..9f98e5dde 100644 --- a/pyatmo.py +++ b/pyatmo/Auth.py @@ -1,25 +1,7 @@ -# Published Jan 2013 -# Revised Jan 2014 (to add new modules data) -# Author : Philippe Larduinat, philippelt@users.sourceforge.net -# Public domain source code -""" -This API provides access to the Netatmo weather station or/and the Netatmo -cameras or/and the Netatmo smart thermostat -This package can be used with Python3 applications -PythonAPI Netatmo REST data access -""" -import logging import time -from smart_home import _BASE_URL, postRequest -from smart_home.Camera import CameraData -from smart_home.Exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule -from smart_home.HomeCoach import HomeCoachData -from smart_home.PublicData import PublicData -from smart_home.Thermostat import HomeData, HomeStatus -from smart_home.WeatherStation import WeatherStationData - -LOG = logging.getLogger(__name__) +from pyatmo import _BASE_URL, postRequest, LOG +from pyatmo.Exceptions import NoDevice # Common definitions @@ -101,65 +83,3 @@ def accessToken(self): self.refreshToken = resp["refresh_token"] self.expiration = int(resp["expire_in"] + time.time() - 1800) return self._accessToken - - -# auto-test when executed directly - -if __name__ == "__main__": - - from sys import exit, stdout, stderr - - try: - import os - - if ( - os.environ["CLIENT_ID"] - and os.environ["CLIENT_SECRET"] - and os.environ["USERNAME"] - and os.environ["PASSWORD"] - ): - CLIENT_ID = os.environ["CLIENT_ID"] - CLIENT_SECRET = os.environ["CLIENT_SECRET"] - USERNAME = os.environ["USERNAME"] - PASSWORD = os.environ["PASSWORD"] - except KeyError: - stderr.write( - "No credentials passed to pyatmo.py (client_id, client_secret, username, password)\n" - ) - exit(1) - - authorization = ClientAuth( - clientId=CLIENT_ID, - clientSecret=CLIENT_SECRET, - username=USERNAME, - password=PASSWORD, - scope="read_station read_camera access_camera write_camera read_thermostat write_thermostat read_presence access_presence read_homecoach", - ) - - try: - ws = WeatherStationData(authorization) - except NoDevice: - if stdout.isatty(): - print("pyatmo.py : warning, no weather station available for testing") - - try: - cam = CameraData(authorization) - except NoDevice: - if stdout.isatty(): - print("pyatmo.py : warning, no camera available for testing") - - try: - hd = HomeData(authorization) - except NoDevice: - if stdout.isatty(): - print("pyatmo.py : warning, no thermostat available for testing") - - PublicData(authorization) - - # If we reach this line, all is OK - - # If launched interactively, display OK message - if stdout.isatty(): - print("pyatmo.py : OK") - - exit(0) diff --git a/smart_home/Camera.py b/pyatmo/Camera.py similarity index 99% rename from smart_home/Camera.py rename to pyatmo/Camera.py index 66f3c3641..6d9a64a62 100644 --- a/smart_home/Camera.py +++ b/pyatmo/Camera.py @@ -1,12 +1,10 @@ import imghdr -import logging import time from urllib.error import URLError -from . import _BASE_URL, postRequest -from .Exceptions import InvalidHome, NoDevice +from pyatmo import _BASE_URL, postRequest, LOG +from pyatmo.Exceptions import InvalidHome, NoDevice -LOG = logging.getLogger(__name__) _GETHOMEDATA_REQ = _BASE_URL + "api/gethomedata" _GETCAMERAPICTURE_REQ = _BASE_URL + "api/getcamerapicture" diff --git a/smart_home/Exceptions.py b/pyatmo/Exceptions.py similarity index 100% rename from smart_home/Exceptions.py rename to pyatmo/Exceptions.py diff --git a/smart_home/HomeCoach.py b/pyatmo/HomeCoach.py similarity index 82% rename from smart_home/HomeCoach.py rename to pyatmo/HomeCoach.py index a3880f8d0..6d88fcb55 100644 --- a/smart_home/HomeCoach.py +++ b/pyatmo/HomeCoach.py @@ -1,5 +1,5 @@ -from . import _BASE_URL -from .WeatherStation import WeatherStationData +from pyatmo import _BASE_URL +from pyatmo.WeatherStation import WeatherStationData _GETHOMECOACHDATA_REQ = _BASE_URL + "api/gethomecoachsdata" diff --git a/smart_home/PublicData.py b/pyatmo/PublicData.py similarity index 98% rename from smart_home/PublicData.py rename to pyatmo/PublicData.py index 3527274e4..edcaf1ab8 100644 --- a/smart_home/PublicData.py +++ b/pyatmo/PublicData.py @@ -1,5 +1,5 @@ -from . import _BASE_URL, postRequest, toTimeString -from .Exceptions import NoDevice +from pyatmo import _BASE_URL, postRequest, toTimeString +from pyatmo.Exceptions import NoDevice _GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata" _LON_NE = 6.221652 diff --git a/smart_home/Thermostat.py b/pyatmo/Thermostat.py similarity index 99% rename from smart_home/Thermostat.py rename to pyatmo/Thermostat.py index 633a618c0..5ba43080b 100644 --- a/smart_home/Thermostat.py +++ b/pyatmo/Thermostat.py @@ -1,7 +1,7 @@ import logging -from . import _BASE_URL, postRequest -from .Exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule +from pyatmo import _BASE_URL, postRequest +from pyatmo.Exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule LOG = logging.getLogger(__name__) diff --git a/smart_home/WeatherStation.py b/pyatmo/WeatherStation.py similarity index 99% rename from smart_home/WeatherStation.py rename to pyatmo/WeatherStation.py index f3f6ac02c..c61e6b624 100644 --- a/smart_home/WeatherStation.py +++ b/pyatmo/WeatherStation.py @@ -1,8 +1,8 @@ import logging import time -from . import _BASE_URL, postRequest, todayStamps -from .Exceptions import NoDevice +from pyatmo import _BASE_URL, postRequest, todayStamps +from pyatmo.Exceptions import NoDevice LOG = logging.getLogger(__name__) diff --git a/smart_home/__init__.py b/pyatmo/__init__.py similarity index 100% rename from smart_home/__init__.py rename to pyatmo/__init__.py diff --git a/pyatmo/__main__.py b/pyatmo/__main__.py new file mode 100644 index 000000000..b45f2acbb --- /dev/null +++ b/pyatmo/__main__.py @@ -0,0 +1,69 @@ +from pyatmo.Auth import ClientAuth +from pyatmo.Camera import CameraData +from pyatmo.Exceptions import NoDevice +from pyatmo.PublicData import PublicData +from pyatmo.Thermostat import HomeData +from pyatmo.WeatherStation import WeatherStationData + + +def main(): + from sys import exit, stdout, stderr + + try: + import os + + if ( + os.environ["CLIENT_ID"] + and os.environ["CLIENT_SECRET"] + and os.environ["USERNAME"] + and os.environ["PASSWORD"] + ): + CLIENT_ID = os.environ["CLIENT_ID"] + CLIENT_SECRET = os.environ["CLIENT_SECRET"] + USERNAME = os.environ["USERNAME"] + PASSWORD = os.environ["PASSWORD"] + except KeyError: + stderr.write( + "No credentials passed to pyatmo.py (client_id, client_secret, username, password)\n" + ) + exit(1) + + authorization = ClientAuth( + clientId=CLIENT_ID, + clientSecret=CLIENT_SECRET, + username=USERNAME, + password=PASSWORD, + scope="read_station read_camera access_camera write_camera read_thermostat write_thermostat read_presence access_presence read_homecoach", + ) + + try: + ws = WeatherStationData(authorization) + except NoDevice: + if stdout.isatty(): + print("pyatmo.py : warning, no weather station available for testing") + + try: + cam = CameraData(authorization) + except NoDevice: + if stdout.isatty(): + print("pyatmo.py : warning, no camera available for testing") + + try: + hd = HomeData(authorization) + except NoDevice: + if stdout.isatty(): + print("pyatmo.py : warning, no thermostat available for testing") + + PublicData(authorization) + + # If we reach this line, all is OK + + # If launched interactively, display OK message + if stdout.isatty(): + print("pyatmo: OK") + + exit(0) + + +if __name__ == "__main__": + main() diff --git a/tests/conftest.py b/tests/conftest.py index 9cde0c545..58cea0ac2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,10 +5,11 @@ import pytest import pyatmo -import smart_home.HomeCoach -import smart_home.PublicData -import smart_home.Thermostat -import smart_home.WeatherStation +import pyatmo.Auth +import pyatmo.HomeCoach +import pyatmo.PublicData +import pyatmo.Thermostat +import pyatmo.WeatherStation @contextmanager @@ -21,11 +22,11 @@ def auth(requests_mock): with open("fixtures/oauth2_token.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo._AUTH_REQ, + pyatmo.Auth._AUTH_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - authorization = pyatmo.ClientAuth( + authorization = pyatmo.Auth.ClientAuth( clientId="CLIENT_ID", clientSecret="CLIENT_SECRET", username="USERNAME", @@ -42,11 +43,11 @@ def homeData(auth, requests_mock): with open("fixtures/home_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Thermostat._GETHOMESDATA_REQ, + pyatmo.Thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - return pyatmo.HomeData(auth) + return pyatmo.Thermostat.HomeData(auth) @pytest.fixture(scope="function") @@ -54,18 +55,18 @@ def homeStatus(auth, requests_mock): with open("fixtures/home_status_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Thermostat._GETHOMESTATUS_REQ, + pyatmo.Thermostat._GETHOMESTATUS_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) with open("fixtures/home_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Thermostat._GETHOMESDATA_REQ, + pyatmo.Thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - return pyatmo.HomeStatus(auth) + return pyatmo.Thermostat.HomeStatus(auth) @pytest.fixture(scope="function") @@ -73,11 +74,11 @@ def publicData(auth, requests_mock): with open("fixtures/public_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.PublicData._GETPUBLIC_DATA, + pyatmo.PublicData._GETPUBLIC_DATA, json=json_fixture, headers={"content-type": "application/json"}, ) - return pyatmo.PublicData(auth) + return pyatmo.PublicData.PublicData(auth) @pytest.fixture(scope="function") @@ -85,11 +86,11 @@ def weatherStationData(auth, requests_mock): with open("fixtures/weatherstation_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.WeatherStation._GETSTATIONDATA_REQ, + pyatmo.WeatherStation._GETSTATIONDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - return pyatmo.WeatherStationData(auth) + return pyatmo.WeatherStation.WeatherStationData(auth) @pytest.fixture(scope="function") @@ -97,11 +98,11 @@ def homeCoachData(auth, requests_mock): with open("fixtures/home_coach_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.HomeCoach._GETHOMECOACHDATA_REQ, + pyatmo.HomeCoach._GETHOMECOACHDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - return pyatmo.HomeCoachData(auth) + return pyatmo.HomeCoach.HomeCoachData(auth) @pytest.fixture(scope="function") @@ -109,8 +110,8 @@ def cameraHomeData(auth, requests_mock): with open("fixtures/camera_home_data.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Camera._GETHOMEDATA_REQ, + pyatmo.Camera._GETHOMEDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - return pyatmo.CameraData(auth) + return pyatmo.Camera.CameraData(auth) diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index 40d5e57e9..f1eba5767 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -1,7 +1,9 @@ -"""Define tests for authentication.""" +"""Define tests for untility methods.""" import json +import time import pytest +import requests import pyatmo @@ -16,14 +18,100 @@ def test_ClientAuth_invalid(requests_mock): with open("fixtures/invalid_grant.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo._AUTH_REQ, + pyatmo.Auth._AUTH_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(pyatmo.NoDevice): - pyatmo.ClientAuth( + with pytest.raises(pyatmo.Exceptions.NoDevice): + pyatmo.Auth.ClientAuth( clientId="CLIENT_ID", clientSecret="CLIENT_SECRET", username="USERNAME", password="PASSWORD", ) + +def test_postRequest_json(requests_mock): + """Test wrapper for posting requests against the Netatmo API.""" + requests_mock.post( + pyatmo._BASE_URL, + json={"a": "b"}, + headers={"content-type": "application/json"}, + ) + resp = pyatmo.postRequest(pyatmo._BASE_URL, None) + assert resp == {"a": "b"} + + +def test_postRequest_binary(requests_mock): + """Test wrapper for posting requests against the Netatmo API.""" + requests_mock.post( + pyatmo._BASE_URL, + text="Success", + headers={"content-type": "application/text"}, + ) + resp = pyatmo.postRequest(pyatmo._BASE_URL, None) + assert resp == b"Success" + + +@pytest.mark.parametrize("test_input,expected", [(200, None), (404, None)]) +def test_postRequest_fail(requests_mock, test_input, expected): + """Test failing requests against the Netatmo API.""" + requests_mock.post(pyatmo._BASE_URL, status_code=test_input) + resp = pyatmo.postRequest(pyatmo._BASE_URL, None) + assert resp is expected + + +def test_postRequest_timeout(requests_mock): + """Test failing requests against the Netatmo API with timeouts.""" + requests_mock.post(pyatmo._BASE_URL, exc=requests.exceptions.ConnectTimeout) + with pytest.raises(requests.exceptions.ConnectTimeout): + assert pyatmo.postRequest(pyatmo._BASE_URL, None) + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (1, "1970-01-01_00:00:01"), + (0, "1970-01-01_00:00:00"), + (-1, "1969-12-31_23:59:59"), + (2000000000, "2033-05-18_03:33:20"), + ("1", "1970-01-01_00:00:01"), + pytest.param("A", None, marks=pytest.mark.xfail), + pytest.param([1], None, marks=pytest.mark.xfail), + pytest.param({1}, None, marks=pytest.mark.xfail), + ], +) +def test_toTimeString(test_input, expected): + """Test time to string conversion.""" + assert pyatmo.toTimeString(test_input) == expected + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("1970-01-01_00:00:01", 1), + ("1970-01-01_00:00:00", 0), + ("1969-12-31_23:59:59", -1), + ("2033-05-18_03:33:20", 2000000000), + ], +) +def test_toEpoch(test_input, expected): + """Test time to epoch conversion.""" + assert pyatmo.toEpoch(test_input) == expected + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("2018-06-21", (1529539200, 1529625600)), + ("2000-01-01", (946684800, 946771200)), + pytest.param("2000-04-31", None, marks=pytest.mark.xfail), + ], +) +def test_todayStamps(monkeypatch, test_input, expected): + """Test todayStamps function.""" + + def mockreturn(format): + return test_input + + monkeypatch.setattr(time, "strftime", mockreturn) + assert pyatmo.todayStamps() == expected diff --git a/tests/test_smart_home_camera.py b/tests/test_pyatmo_camera.py similarity index 96% rename from tests/test_smart_home_camera.py rename to tests/test_pyatmo_camera.py index 79d45e52f..d0579ba86 100644 --- a/tests/test_smart_home_camera.py +++ b/tests/test_pyatmo_camera.py @@ -4,9 +4,8 @@ import pytest from freezegun import freeze_time -import smart_home.Camera +import pyatmo.Camera -from .conftest import does_not_raise INVALID_NAME = "InvalidName" @@ -43,7 +42,7 @@ def test_CameraData_homeById(cameraHomeData, hid, expected): ) def test_CameraData_homeByName(cameraHomeData, name, expected): if name == INVALID_NAME: - with pytest.raises(smart_home.Exceptions.InvalidHome): + with pytest.raises(pyatmo.Exceptions.InvalidHome): assert cameraHomeData.homeByName(name) else: assert cameraHomeData.homeByName(name)["id"] == expected @@ -147,11 +146,11 @@ def test_CameraData_cameraUrls_disconnected(auth, requests_mock): with open("fixtures/camera_home_data_disconnected.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Camera._GETHOMEDATA_REQ, + pyatmo.Camera._GETHOMEDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - camera_data = smart_home.Camera.CameraData(auth) + camera_data = pyatmo.Camera.CameraData(auth) assert camera_data.cameraUrls() == (None, None) @@ -169,7 +168,7 @@ def test_CameraData_cameraUrls_disconnected(auth, requests_mock): ) def test_CameraData_personsAtHome(cameraHomeData, home, expected): if home == INVALID_NAME: - with pytest.raises(smart_home.Exceptions.InvalidHome): + with pytest.raises(pyatmo.Exceptions.InvalidHome): assert cameraHomeData.personsAtHome(home) else: assert cameraHomeData.personsAtHome(home) == expected @@ -334,5 +333,5 @@ def test_CameraData_getHomeName(cameraHomeData): def test_CameraData_gethomeId(cameraHomeData): assert cameraHomeData.gethomeId() == "91763b24c43d3e344f424e8b" assert cameraHomeData.gethomeId("MYHOME") == "91763b24c43d3e344f424e8b" - with pytest.raises(smart_home.Camera.InvalidHome): + with pytest.raises(pyatmo.Camera.InvalidHome): assert cameraHomeData.gethomeId("InvalidName") diff --git a/tests/test_smart_home_homecoach.py b/tests/test_pyatmo_homecoach.py similarity index 93% rename from tests/test_smart_home_homecoach.py rename to tests/test_pyatmo_homecoach.py index 3fad787cb..13423268f 100644 --- a/tests/test_smart_home_homecoach.py +++ b/tests/test_pyatmo_homecoach.py @@ -3,7 +3,7 @@ import pytest -import smart_home.HomeCoach +import pyatmo.HomeCoach def test_HomeCoachData(homeCoachData): @@ -88,9 +88,9 @@ def test_HomeCoachData_no_devices(auth, requests_mock): with open("fixtures/home_coach_no_devices.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.HomeCoach._GETHOMECOACHDATA_REQ, + pyatmo.HomeCoach._GETHOMECOACHDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(smart_home.WeatherStation.NoDevice): - assert smart_home.HomeCoach.HomeCoachData(auth) + with pytest.raises(pyatmo.WeatherStation.NoDevice): + assert pyatmo.HomeCoach.HomeCoachData(auth) diff --git a/tests/test_smart_home_publicdata.py b/tests/test_pyatmo_publicdata.py similarity index 93% rename from tests/test_smart_home_publicdata.py rename to tests/test_pyatmo_publicdata.py index d19627623..8f41044e8 100644 --- a/tests/test_smart_home_publicdata.py +++ b/tests/test_pyatmo_publicdata.py @@ -3,42 +3,42 @@ import pytest -import smart_home.PublicData +import pyatmo.PublicData def test_PublicData(auth, requests_mock): with open("fixtures/public_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.PublicData._GETPUBLIC_DATA, + pyatmo.PublicData._GETPUBLIC_DATA, json=json_fixture, headers={"content-type": "application/json"}, ) - publicData = smart_home.PublicData.PublicData(auth) + publicData = pyatmo.PublicData.PublicData(auth) assert publicData.status == "ok" - publicData = smart_home.PublicData.PublicData( + publicData = pyatmo.PublicData.PublicData( auth, required_data_type="temperature,rain_live" ) assert publicData.status == "ok" def test_PublicData_unavailable(auth, requests_mock): - requests_mock.post(smart_home.PublicData._GETPUBLIC_DATA, status_code=404) - with pytest.raises(smart_home.PublicData.NoDevice): - smart_home.PublicData.PublicData(auth) + requests_mock.post(pyatmo.PublicData._GETPUBLIC_DATA, status_code=404) + with pytest.raises(pyatmo.PublicData.NoDevice): + pyatmo.PublicData.PublicData(auth) def test_PublicData_error(auth, requests_mock): with open("fixtures/public_data_error_mongo.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.PublicData._GETPUBLIC_DATA, + pyatmo.PublicData._GETPUBLIC_DATA, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(smart_home.PublicData.NoDevice): - smart_home.PublicData.PublicData(auth) + with pytest.raises(pyatmo.PublicData.NoDevice): + pyatmo.PublicData.PublicData(auth) def test_PublicData_CountStationInArea(publicData): @@ -297,4 +297,4 @@ def test_PublicData_getAccessoryMeasures(publicData, test_input, expected): ], ) def test_PublicData_averageMeasure(test_input, expected): - assert smart_home.PublicData.averageMeasure(test_input) == expected + assert pyatmo.PublicData.averageMeasure(test_input) == expected diff --git a/tests/test_smart_home_thermostat.py b/tests/test_pyatmo_thermostat.py similarity index 87% rename from tests/test_smart_home_thermostat.py rename to tests/test_pyatmo_thermostat.py index 1ce999b7a..0c748e40d 100644 --- a/tests/test_smart_home_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -4,8 +4,8 @@ import pytest -import smart_home.Exceptions -import smart_home.Thermostat +import pyatmo.Exceptions +import pyatmo.Thermostat from .conftest import does_not_raise @@ -65,32 +65,32 @@ def test_HomeData(homeData): def test_HomeData_no_data(auth, requests_mock): - requests_mock.post(smart_home.Thermostat._GETHOMESDATA_REQ, text="None") - with pytest.raises(smart_home.Thermostat.NoDevice): - assert smart_home.Thermostat.HomeData(auth) + requests_mock.post(pyatmo.Thermostat._GETHOMESDATA_REQ, text="None") + with pytest.raises(pyatmo.Thermostat.NoDevice): + assert pyatmo.Thermostat.HomeData(auth) def test_HomeData_no_body(auth, requests_mock): with open("fixtures/home_data_empty.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Thermostat._GETHOMESDATA_REQ, + pyatmo.Thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(smart_home.Thermostat.NoDevice): - assert smart_home.Thermostat.HomeData(auth) + with pytest.raises(pyatmo.Thermostat.NoDevice): + assert pyatmo.Thermostat.HomeData(auth) def test_HomeData_no_home_name(auth, requests_mock): with open("fixtures/home_data_nohomename.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Thermostat._GETHOMESDATA_REQ, + pyatmo.Thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - homeData = smart_home.Thermostat.HomeData(auth) + homeData = pyatmo.Thermostat.HomeData(auth) home_id = "91763b24c43d3e344f424e8b" assert homeData.homeById(home_id)["name"] == "Unknown" @@ -110,7 +110,7 @@ def test_HomeData_homeByName(homeData): def test_HomeData_gethomeId(homeData): assert homeData.gethomeId() == "91763b24c43d3e344f424e8b" assert homeData.gethomeId("MYHOME") == "91763b24c43d3e344f424e8b" - with pytest.raises(smart_home.Thermostat.InvalidHome): + with pytest.raises(pyatmo.Thermostat.InvalidHome): assert homeData.gethomeId("InvalidName") @@ -125,22 +125,22 @@ def test_HomeData_getHomeName(homeData): def test_HomeData_getSelectedschedule(homeData): assert homeData.getSelectedschedule()["name"] == "Default" assert homeData.getSelectedschedule("MYHOME")["name"] == "Default" - with pytest.raises(smart_home.Exceptions.InvalidHome): + with pytest.raises(pyatmo.Exceptions.InvalidHome): assert homeData.getSelectedschedule("Unknown") @pytest.mark.parametrize( "t_home, t_sched_id, t_sched, expected", [ - (None, None, None, pytest.raises(smart_home.Exceptions.NoSchedule)), + (None, None, None, pytest.raises(pyatmo.Exceptions.NoSchedule)), (None, None, "Default", does_not_raise()), (None, "591b54a2764ff4d50d8b5795", None, does_not_raise()), - (None, None, "Summer", pytest.raises(smart_home.Exceptions.NoSchedule)), + (None, None, "Summer", pytest.raises(pyatmo.Exceptions.NoSchedule)), ( None, "123456789abcdefg12345678", None, - pytest.raises(smart_home.Exceptions.NoSchedule), + pytest.raises(pyatmo.Exceptions.NoSchedule), ), ], ) @@ -150,7 +150,7 @@ def test_HomeData_switchHomeSchedule( with open("fixtures/status_ok.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Thermostat._SWITCHHOMESCHEDULE_REQ, + pyatmo.Thermostat._SWITCHHOMESCHEDULE_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) @@ -180,18 +180,18 @@ def test_HomeStatus_error_and_data(auth, requests_mock): with open("fixtures/home_status_error_and_data.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Thermostat._GETHOMESTATUS_REQ, + pyatmo.Thermostat._GETHOMESTATUS_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) with open("fixtures/home_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Thermostat._GETHOMESDATA_REQ, + pyatmo.Thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - homeStatus = smart_home.Thermostat.HomeStatus(auth) + homeStatus = pyatmo.Thermostat.HomeStatus(auth) assert len(homeStatus.rooms) == 3 assert homeStatus.default_room["id"] == "2746182631" @@ -211,19 +211,19 @@ def test_HomeStatus_error(auth, requests_mock): with open("fixtures/home_status_empty.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Thermostat._GETHOMESTATUS_REQ, + pyatmo.Thermostat._GETHOMESTATUS_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) with open("fixtures/home_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Thermostat._GETHOMESDATA_REQ, + pyatmo.Thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(smart_home.Exceptions.NoDevice): - assert smart_home.Thermostat.HomeStatus(auth) + with pytest.raises(pyatmo.Exceptions.NoDevice): + assert pyatmo.Thermostat.HomeStatus(auth) def test_HomeStatus_roomById(homeStatus): @@ -237,7 +237,7 @@ def test_HomeStatus_roomById(homeStatus): "therm_setpoint_end_time": 0, } assert homeStatus.roomById("2746182631") == expexted - with pytest.raises(smart_home.Exceptions.InvalidRoom): + with pytest.raises(pyatmo.Exceptions.InvalidRoom): assert homeStatus.roomById("0000000000") @@ -256,7 +256,7 @@ def test_HomeStatus_thermostatById(homeStatus): "battery_state": "high", } assert homeStatus.thermostatById("12:34:56:00:01:ae") == expexted - with pytest.raises(smart_home.Exceptions.InvalidRoom): + with pytest.raises(pyatmo.Exceptions.InvalidRoom): assert homeStatus.thermostatById("00:00:00:00:00:00") @@ -269,7 +269,7 @@ def test_HomeStatus_relayById(homeStatus): "wifi_strength": 42, } assert homeStatus.relayById("12:34:56:00:fa:d0") == expexted - with pytest.raises(smart_home.Exceptions.InvalidRoom): + with pytest.raises(pyatmo.Exceptions.InvalidRoom): assert homeStatus.relayById("00:00:00:00:00:00") @@ -285,14 +285,14 @@ def test_HomeStatus_valveById(homeStatus): "battery_state": "full", } assert homeStatus.valveById("12:34:56:03:a5:54") == expexted - with pytest.raises(smart_home.Exceptions.InvalidRoom): + with pytest.raises(pyatmo.Exceptions.InvalidRoom): assert homeStatus.valveById("00:00:00:00:00:00") def test_HomeStatus_setPoint(homeStatus): assert homeStatus.setPoint() == 12 assert homeStatus.setPoint("2746182631") == 12 - with pytest.raises(smart_home.Exceptions.InvalidRoom): + with pytest.raises(pyatmo.Exceptions.InvalidRoom): assert homeStatus.setPoint("0000000000") @@ -313,7 +313,7 @@ def test_HomeStatus_getAwaytemp(homeStatus): def test_HomeStatus_getHgtemp(homeStatus): assert homeStatus.getHgtemp() == 7 assert homeStatus.getHgtemp("MYHOME") == 7 - with pytest.raises(smart_home.Exceptions.InvalidHome): + with pytest.raises(pyatmo.Exceptions.InvalidHome): assert homeStatus.getHgtemp("InvalidHome") assert homeStatus.getHgtemp(home_id="91763b24c43d3e344f424e8b") == 7 assert homeStatus.getHgtemp(home_id="00000000000000000000000") is None @@ -322,7 +322,7 @@ def test_HomeStatus_getHgtemp(homeStatus): def test_HomeStatus_measuredTemperature(homeStatus): assert homeStatus.measuredTemperature() == 19.8 assert homeStatus.measuredTemperature("2746182631") == 19.8 - with pytest.raises(smart_home.Exceptions.InvalidRoom): + with pytest.raises(pyatmo.Exceptions.InvalidRoom): assert homeStatus.measuredTemperature("0000000000") @@ -333,7 +333,7 @@ def test_HomeStatus_boilerStatus(homeStatus): def test_HomeStatus_thermostatType(homeStatus): assert homeStatus.thermostatType("MYHOME", "2746182631") == "NATherm1" assert homeStatus.thermostatType("MYHOME", "2833524037") == "NRV" - with pytest.raises(smart_home.Exceptions.InvalidHome): + with pytest.raises(pyatmo.Exceptions.InvalidHome): assert homeStatus.thermostatType("InvalidHome", "2833524037") assert homeStatus.thermostatType("MYHOME", "0000000000") is None @@ -358,7 +358,7 @@ def test_HomeData_setThermmode( with open("fixtures/%s" % json_fixture) as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Thermostat._SETTHERMMODE_REQ, + pyatmo.Thermostat._SETTHERMMODE_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) @@ -395,7 +395,7 @@ def test_HomeData_setroomThermpoint( with open("fixtures/%s" % json_fixture) as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Thermostat._SETROOMTHERMPOINT_REQ, + pyatmo.Thermostat._SETROOMTHERMPOINT_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) @@ -458,7 +458,7 @@ def test_HomeData_setroomThermpoint_error( with open("fixtures/%s" % json_fixture) as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Thermostat._SETROOMTHERMPOINT_REQ, + pyatmo.Thermostat._SETROOMTHERMPOINT_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) diff --git a/tests/test_smart_home_weatherstation.py b/tests/test_pyatmo_weatherstation.py similarity index 95% rename from tests/test_smart_home_weatherstation.py rename to tests/test_pyatmo_weatherstation.py index e593e747a..3c9d5dc94 100644 --- a/tests/test_smart_home_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -4,7 +4,7 @@ import pytest from freezegun import freeze_time -import smart_home.WeatherStation +import pyatmo.WeatherStation from .conftest import does_not_raise @@ -14,33 +14,33 @@ def test_WeatherStationData(weatherStationData): def test_WeatherStationData_no_response(auth, requests_mock): - requests_mock.post(smart_home.WeatherStation._GETSTATIONDATA_REQ, text="None") - with pytest.raises(smart_home.WeatherStation.NoDevice): - assert smart_home.WeatherStation.WeatherStationData(auth) + requests_mock.post(pyatmo.WeatherStation._GETSTATIONDATA_REQ, text="None") + with pytest.raises(pyatmo.WeatherStation.NoDevice): + assert pyatmo.WeatherStation.WeatherStationData(auth) def test_WeatherStationData_no_body(auth, requests_mock): with open("fixtures/status_ok.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.WeatherStation._GETSTATIONDATA_REQ, + pyatmo.WeatherStation._GETSTATIONDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(smart_home.WeatherStation.NoDevice): - assert smart_home.WeatherStation.WeatherStationData(auth) + with pytest.raises(pyatmo.WeatherStation.NoDevice): + assert pyatmo.WeatherStation.WeatherStationData(auth) def test_WeatherStationData_no_data(auth, requests_mock): with open("fixtures/home_data_empty.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.WeatherStation._GETSTATIONDATA_REQ, + pyatmo.WeatherStation._GETSTATIONDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(smart_home.WeatherStation.NoDevice): - assert smart_home.WeatherStation.WeatherStationData(auth) + with pytest.raises(pyatmo.WeatherStation.NoDevice): + assert pyatmo.WeatherStation.WeatherStationData(auth) @pytest.mark.parametrize( @@ -519,7 +519,7 @@ def test_WeatherStationData_getMeasure( with open("fixtures/weatherstation_measure.json") as f: json_fixture = json.load(f) requests_mock.post( - smart_home.WeatherStation._GETMEASURE_REQ, + pyatmo.WeatherStation._GETMEASURE_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) diff --git a/tests/test_smart_home.py b/tests/test_smart_home.py deleted file mode 100644 index 1643e32b8..000000000 --- a/tests/test_smart_home.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Define tests for untility methods.""" -import time - -import pytest -import requests - -import smart_home - - -def test_postRequest_json(requests_mock): - """Test wrapper for posting requests against the Netatmo API.""" - requests_mock.post( - smart_home._BASE_URL, - json={"a": "b"}, - headers={"content-type": "application/json"}, - ) - resp = smart_home.postRequest(smart_home._BASE_URL, None) - assert resp == {"a": "b"} - - -def test_postRequest_binary(requests_mock): - """Test wrapper for posting requests against the Netatmo API.""" - requests_mock.post( - smart_home._BASE_URL, - text="Success", - headers={"content-type": "application/text"}, - ) - resp = smart_home.postRequest(smart_home._BASE_URL, None) - assert resp == b"Success" - - -@pytest.mark.parametrize("test_input,expected", [(200, None), (404, None)]) -def test_postRequest_fail(requests_mock, test_input, expected): - """Test failing requests against the Netatmo API.""" - requests_mock.post(smart_home._BASE_URL, status_code=test_input) - resp = smart_home.postRequest(smart_home._BASE_URL, None) - assert resp is expected - - -def test_postRequest_timeout(requests_mock): - """Test failing requests against the Netatmo API with timeouts.""" - requests_mock.post(smart_home._BASE_URL, exc=requests.exceptions.ConnectTimeout) - with pytest.raises(requests.exceptions.ConnectTimeout): - assert smart_home.postRequest(smart_home._BASE_URL, None) - - -@pytest.mark.parametrize( - "test_input,expected", - [ - (1, "1970-01-01_00:00:01"), - (0, "1970-01-01_00:00:00"), - (-1, "1969-12-31_23:59:59"), - (2000000000, "2033-05-18_03:33:20"), - ("1", "1970-01-01_00:00:01"), - pytest.param("A", None, marks=pytest.mark.xfail), - pytest.param([1], None, marks=pytest.mark.xfail), - pytest.param({1}, None, marks=pytest.mark.xfail), - ], -) -def test_toTimeString(test_input, expected): - """Test time to string conversion.""" - assert smart_home.toTimeString(test_input) == expected - - -@pytest.mark.parametrize( - "test_input,expected", - [ - ("1970-01-01_00:00:01", 1), - ("1970-01-01_00:00:00", 0), - ("1969-12-31_23:59:59", -1), - ("2033-05-18_03:33:20", 2000000000), - ], -) -def test_toEpoch(test_input, expected): - """Test time to epoch conversion.""" - assert smart_home.toEpoch(test_input) == expected - - -@pytest.mark.parametrize( - "test_input,expected", - [ - ("2018-06-21", (1529539200, 1529625600)), - ("2000-01-01", (946684800, 946771200)), - pytest.param("2000-04-31", None, marks=pytest.mark.xfail), - ], -) -def test_todayStamps(monkeypatch, test_input, expected): - """Test todayStamps function.""" - - def mockreturn(format): - return test_input - - monkeypatch.setattr(time, "strftime", mockreturn) - assert smart_home.todayStamps() == expected From 810c293c3075ad6849099024cba39134144a83f5 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 31 Oct 2019 12:29:40 +0100 Subject: [PATCH 142/333] More refactoring --- .gitignore | 5 +- pyatmo/__init__.py | 96 +++++-------------- pyatmo/__main__.py | 12 +-- pyatmo/{Auth.py => auth.py} | 4 +- pyatmo/{Camera.py => camera.py} | 4 +- pyatmo/{Exceptions.py => exceptions.py} | 0 pyatmo/helpers.py | 74 ++++++++++++++ pyatmo/{HomeCoach.py => home_coach.py} | 4 +- pyatmo/{PublicData.py => public_data.py} | 4 +- pyatmo/{Thermostat.py => thermostat.py} | 4 +- .../{WeatherStation.py => weather_station.py} | 4 +- setup.py | 8 +- tests/conftest.py | 35 +++---- tests/test_pyatmo.py | 28 +++--- tests/test_pyatmo_camera.py | 12 +-- tests/test_pyatmo_homecoach.py | 8 +- tests/test_pyatmo_publicdata.py | 22 ++--- tests/test_pyatmo_thermostat.py | 67 +++++++------ tests/test_pyatmo_weatherstation.py | 24 +++-- 19 files changed, 215 insertions(+), 200 deletions(-) rename pyatmo/{Auth.py => auth.py} (97%) rename pyatmo/{Camera.py => camera.py} (99%) rename pyatmo/{Exceptions.py => exceptions.py} (100%) create mode 100644 pyatmo/helpers.py rename pyatmo/{HomeCoach.py => home_coach.py} (82%) rename pyatmo/{PublicData.py => public_data.py} (98%) rename pyatmo/{Thermostat.py => thermostat.py} (99%) rename pyatmo/{WeatherStation.py => weather_station.py} (99%) diff --git a/.gitignore b/.gitignore index e1473d8d1..25d590d25 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ pyatmo.egg-info/ *.pyc .DS_Store -venv/ \ No newline at end of file +venv/ + +.mypy_cache/ +.pytest_cache/ \ No newline at end of file diff --git a/pyatmo/__init__.py b/pyatmo/__init__.py index 41f25b222..de0990e8c 100644 --- a/pyatmo/__init__.py +++ b/pyatmo/__init__.py @@ -1,74 +1,22 @@ -import logging -import time -from calendar import timegm -from datetime import datetime - -import requests - -LOG = logging.getLogger(__name__) - -# Common definitions -_BASE_URL = "https://api.netatmo.com/" - - -# Utilities routines - - -def postRequest(url, params=None, timeout=10): - resp = requests.post(url, data=params, timeout=timeout) - if not resp.ok: - LOG.error("The Netatmo API returned %s", resp.status_code) - try: - return ( - resp.json() - if "application/json" in resp.headers.get("content-type") - else resp.content - ) - except TypeError: - LOG.debug("Invalid response %s", resp) - return None - - -def toTimeString(value): - return datetime.utcfromtimestamp(int(value)).isoformat(sep="_") - - -def toEpoch(value): - return timegm(time.strptime(value + "GMT", "%Y-%m-%d_%H:%M:%S%Z")) - - -def todayStamps(): - today = timegm(time.strptime(time.strftime("%Y-%m-%d") + "GMT", "%Y-%m-%d%Z")) - return today, today + 3600 * 24 - - -# Global shortcut - - -def getStationMinMaxTH(station=None, module=None): - from pyatmo import ClientAuth - from smart_home.WeatherStation import DeviceList - - authorization = ClientAuth() - devList = DeviceList(authorization) - if not station: - station = devList.default_station - if module: - mname = module - else: - mname = devList.stationByName(station)["module_name"] - lastD = devList.lastData(station) - if mname == "*": - result = {} - for m in lastD.keys(): - if time.time() - lastD[m]["When"] > 3600: - continue - r = devList.MinMaxTH(module=m) - result[m] = (r[0], lastD[m]["Temperature"], r[1]) - else: - if time.time() - lastD[mname]["When"] > 3600: - result = ["-", "-"] - else: - result = [lastD[mname]["Temperature"], lastD[mname]["Humidity"]] - result.extend(devList.MinMaxTH(station, mname)) - return result +from .auth import ClientAuth +from .exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule +from .weather_station import WeatherStationData +from .thermostat import HomeData, HomeStatus +from .camera import CameraData +from .home_coach import HomeCoachData +from .public_data import PublicData + + +__all__ = [ + "ClientAuth", + "WeatherStationData", + "HomeData", + "HomeStatus", + "InvalidHome", + "InvalidRoom", + "NoDevice", + "NoSchedule", + "CameraData", + "HomeCoachData", + "PublicData", +] diff --git a/pyatmo/__main__.py b/pyatmo/__main__.py index b45f2acbb..c067ca609 100644 --- a/pyatmo/__main__.py +++ b/pyatmo/__main__.py @@ -1,9 +1,9 @@ -from pyatmo.Auth import ClientAuth -from pyatmo.Camera import CameraData -from pyatmo.Exceptions import NoDevice -from pyatmo.PublicData import PublicData -from pyatmo.Thermostat import HomeData -from pyatmo.WeatherStation import WeatherStationData +from .auth import ClientAuth +from .camera import CameraData +from .exceptions import NoDevice +from .public_data import PublicData +from .thermostat import HomeData +from .weather_station import WeatherStationData def main(): diff --git a/pyatmo/Auth.py b/pyatmo/auth.py similarity index 97% rename from pyatmo/Auth.py rename to pyatmo/auth.py index 9f98e5dde..5529b849f 100644 --- a/pyatmo/Auth.py +++ b/pyatmo/auth.py @@ -1,7 +1,7 @@ import time -from pyatmo import _BASE_URL, postRequest, LOG -from pyatmo.Exceptions import NoDevice +from .helpers import _BASE_URL, postRequest, LOG +from .exceptions import NoDevice # Common definitions diff --git a/pyatmo/Camera.py b/pyatmo/camera.py similarity index 99% rename from pyatmo/Camera.py rename to pyatmo/camera.py index 6d9a64a62..dbdc67920 100644 --- a/pyatmo/Camera.py +++ b/pyatmo/camera.py @@ -2,8 +2,8 @@ import time from urllib.error import URLError -from pyatmo import _BASE_URL, postRequest, LOG -from pyatmo.Exceptions import InvalidHome, NoDevice +from .helpers import _BASE_URL, postRequest, LOG +from .exceptions import InvalidHome, NoDevice _GETHOMEDATA_REQ = _BASE_URL + "api/gethomedata" diff --git a/pyatmo/Exceptions.py b/pyatmo/exceptions.py similarity index 100% rename from pyatmo/Exceptions.py rename to pyatmo/exceptions.py diff --git a/pyatmo/helpers.py b/pyatmo/helpers.py new file mode 100644 index 000000000..41f25b222 --- /dev/null +++ b/pyatmo/helpers.py @@ -0,0 +1,74 @@ +import logging +import time +from calendar import timegm +from datetime import datetime + +import requests + +LOG = logging.getLogger(__name__) + +# Common definitions +_BASE_URL = "https://api.netatmo.com/" + + +# Utilities routines + + +def postRequest(url, params=None, timeout=10): + resp = requests.post(url, data=params, timeout=timeout) + if not resp.ok: + LOG.error("The Netatmo API returned %s", resp.status_code) + try: + return ( + resp.json() + if "application/json" in resp.headers.get("content-type") + else resp.content + ) + except TypeError: + LOG.debug("Invalid response %s", resp) + return None + + +def toTimeString(value): + return datetime.utcfromtimestamp(int(value)).isoformat(sep="_") + + +def toEpoch(value): + return timegm(time.strptime(value + "GMT", "%Y-%m-%d_%H:%M:%S%Z")) + + +def todayStamps(): + today = timegm(time.strptime(time.strftime("%Y-%m-%d") + "GMT", "%Y-%m-%d%Z")) + return today, today + 3600 * 24 + + +# Global shortcut + + +def getStationMinMaxTH(station=None, module=None): + from pyatmo import ClientAuth + from smart_home.WeatherStation import DeviceList + + authorization = ClientAuth() + devList = DeviceList(authorization) + if not station: + station = devList.default_station + if module: + mname = module + else: + mname = devList.stationByName(station)["module_name"] + lastD = devList.lastData(station) + if mname == "*": + result = {} + for m in lastD.keys(): + if time.time() - lastD[m]["When"] > 3600: + continue + r = devList.MinMaxTH(module=m) + result[m] = (r[0], lastD[m]["Temperature"], r[1]) + else: + if time.time() - lastD[mname]["When"] > 3600: + result = ["-", "-"] + else: + result = [lastD[mname]["Temperature"], lastD[mname]["Humidity"]] + result.extend(devList.MinMaxTH(station, mname)) + return result diff --git a/pyatmo/HomeCoach.py b/pyatmo/home_coach.py similarity index 82% rename from pyatmo/HomeCoach.py rename to pyatmo/home_coach.py index 6d88fcb55..21e11180d 100644 --- a/pyatmo/HomeCoach.py +++ b/pyatmo/home_coach.py @@ -1,5 +1,5 @@ -from pyatmo import _BASE_URL -from pyatmo.WeatherStation import WeatherStationData +from .helpers import _BASE_URL +from .weather_station import WeatherStationData _GETHOMECOACHDATA_REQ = _BASE_URL + "api/gethomecoachsdata" diff --git a/pyatmo/PublicData.py b/pyatmo/public_data.py similarity index 98% rename from pyatmo/PublicData.py rename to pyatmo/public_data.py index edcaf1ab8..040ba0e52 100644 --- a/pyatmo/PublicData.py +++ b/pyatmo/public_data.py @@ -1,5 +1,5 @@ -from pyatmo import _BASE_URL, postRequest, toTimeString -from pyatmo.Exceptions import NoDevice +from .helpers import _BASE_URL, postRequest, toTimeString +from .exceptions import NoDevice _GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata" _LON_NE = 6.221652 diff --git a/pyatmo/Thermostat.py b/pyatmo/thermostat.py similarity index 99% rename from pyatmo/Thermostat.py rename to pyatmo/thermostat.py index 5ba43080b..630da7ee7 100644 --- a/pyatmo/Thermostat.py +++ b/pyatmo/thermostat.py @@ -1,7 +1,7 @@ import logging -from pyatmo import _BASE_URL, postRequest -from pyatmo.Exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule +from .helpers import _BASE_URL, postRequest +from .exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule LOG = logging.getLogger(__name__) diff --git a/pyatmo/WeatherStation.py b/pyatmo/weather_station.py similarity index 99% rename from pyatmo/WeatherStation.py rename to pyatmo/weather_station.py index c61e6b624..3455c9d9e 100644 --- a/pyatmo/WeatherStation.py +++ b/pyatmo/weather_station.py @@ -1,8 +1,8 @@ import logging import time -from pyatmo import _BASE_URL, postRequest, todayStamps -from pyatmo.Exceptions import NoDevice +from .helpers import _BASE_URL, postRequest, todayStamps +from .exceptions import NoDevice LOG = logging.getLogger(__name__) diff --git a/setup.py b/setup.py index 15b03eba5..c0b4cafc5 100755 --- a/setup.py +++ b/setup.py @@ -2,16 +2,14 @@ from distutils.core import setup -import setuptools - setup( name="pyatmo", - version="2.3.2", # Should be updated with new versions + version="3.0.0", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], - packages=["smart_home"], - package_dir={"smart_home": "smart_home"}, + packages=["pyatmo"], + package_dir={"pyatmo": "pyatmo"}, scripts=[], data_files=[], url="https://github.com/jabesq/netatmo-api-python", diff --git a/tests/conftest.py b/tests/conftest.py index 58cea0ac2..81f66032b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,11 +5,6 @@ import pytest import pyatmo -import pyatmo.Auth -import pyatmo.HomeCoach -import pyatmo.PublicData -import pyatmo.Thermostat -import pyatmo.WeatherStation @contextmanager @@ -22,11 +17,11 @@ def auth(requests_mock): with open("fixtures/oauth2_token.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Auth._AUTH_REQ, + pyatmo.auth._AUTH_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - authorization = pyatmo.Auth.ClientAuth( + authorization = pyatmo.ClientAuth( clientId="CLIENT_ID", clientSecret="CLIENT_SECRET", username="USERNAME", @@ -43,11 +38,11 @@ def homeData(auth, requests_mock): with open("fixtures/home_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Thermostat._GETHOMESDATA_REQ, + pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - return pyatmo.Thermostat.HomeData(auth) + return pyatmo.HomeData(auth) @pytest.fixture(scope="function") @@ -55,18 +50,18 @@ def homeStatus(auth, requests_mock): with open("fixtures/home_status_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Thermostat._GETHOMESTATUS_REQ, + pyatmo.thermostat._GETHOMESTATUS_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) with open("fixtures/home_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Thermostat._GETHOMESDATA_REQ, + pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - return pyatmo.Thermostat.HomeStatus(auth) + return pyatmo.HomeStatus(auth) @pytest.fixture(scope="function") @@ -74,11 +69,11 @@ def publicData(auth, requests_mock): with open("fixtures/public_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.PublicData._GETPUBLIC_DATA, + pyatmo.public_data._GETPUBLIC_DATA, json=json_fixture, headers={"content-type": "application/json"}, ) - return pyatmo.PublicData.PublicData(auth) + return pyatmo.PublicData(auth) @pytest.fixture(scope="function") @@ -86,11 +81,11 @@ def weatherStationData(auth, requests_mock): with open("fixtures/weatherstation_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.WeatherStation._GETSTATIONDATA_REQ, + pyatmo.weather_station._GETSTATIONDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - return pyatmo.WeatherStation.WeatherStationData(auth) + return pyatmo.WeatherStationData(auth) @pytest.fixture(scope="function") @@ -98,11 +93,11 @@ def homeCoachData(auth, requests_mock): with open("fixtures/home_coach_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.HomeCoach._GETHOMECOACHDATA_REQ, + pyatmo.home_coach._GETHOMECOACHDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - return pyatmo.HomeCoach.HomeCoachData(auth) + return pyatmo.HomeCoachData(auth) @pytest.fixture(scope="function") @@ -110,8 +105,8 @@ def cameraHomeData(auth, requests_mock): with open("fixtures/camera_home_data.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Camera._GETHOMEDATA_REQ, + pyatmo.camera._GETHOMEDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - return pyatmo.Camera.CameraData(auth) + return pyatmo.CameraData(auth) diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index f1eba5767..70b68b2be 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -18,12 +18,12 @@ def test_ClientAuth_invalid(requests_mock): with open("fixtures/invalid_grant.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Auth._AUTH_REQ, + pyatmo.auth._AUTH_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(pyatmo.Exceptions.NoDevice): - pyatmo.Auth.ClientAuth( + with pytest.raises(pyatmo.exceptions.NoDevice): + pyatmo.ClientAuth( clientId="CLIENT_ID", clientSecret="CLIENT_SECRET", username="USERNAME", @@ -33,38 +33,38 @@ def test_ClientAuth_invalid(requests_mock): def test_postRequest_json(requests_mock): """Test wrapper for posting requests against the Netatmo API.""" requests_mock.post( - pyatmo._BASE_URL, + pyatmo.helpers._BASE_URL, json={"a": "b"}, headers={"content-type": "application/json"}, ) - resp = pyatmo.postRequest(pyatmo._BASE_URL, None) + resp = pyatmo.helpers.postRequest(pyatmo.helpers._BASE_URL, None) assert resp == {"a": "b"} def test_postRequest_binary(requests_mock): """Test wrapper for posting requests against the Netatmo API.""" requests_mock.post( - pyatmo._BASE_URL, + pyatmo.helpers._BASE_URL, text="Success", headers={"content-type": "application/text"}, ) - resp = pyatmo.postRequest(pyatmo._BASE_URL, None) + resp = pyatmo.helpers.postRequest(pyatmo.helpers._BASE_URL, None) assert resp == b"Success" @pytest.mark.parametrize("test_input,expected", [(200, None), (404, None)]) def test_postRequest_fail(requests_mock, test_input, expected): """Test failing requests against the Netatmo API.""" - requests_mock.post(pyatmo._BASE_URL, status_code=test_input) - resp = pyatmo.postRequest(pyatmo._BASE_URL, None) + requests_mock.post(pyatmo.helpers._BASE_URL, status_code=test_input) + resp = pyatmo.helpers.postRequest(pyatmo.helpers._BASE_URL, None) assert resp is expected def test_postRequest_timeout(requests_mock): """Test failing requests against the Netatmo API with timeouts.""" - requests_mock.post(pyatmo._BASE_URL, exc=requests.exceptions.ConnectTimeout) + requests_mock.post(pyatmo.helpers._BASE_URL, exc=requests.exceptions.ConnectTimeout) with pytest.raises(requests.exceptions.ConnectTimeout): - assert pyatmo.postRequest(pyatmo._BASE_URL, None) + assert pyatmo.helpers.postRequest(pyatmo.helpers._BASE_URL, None) @pytest.mark.parametrize( @@ -82,7 +82,7 @@ def test_postRequest_timeout(requests_mock): ) def test_toTimeString(test_input, expected): """Test time to string conversion.""" - assert pyatmo.toTimeString(test_input) == expected + assert pyatmo.helpers.toTimeString(test_input) == expected @pytest.mark.parametrize( @@ -96,7 +96,7 @@ def test_toTimeString(test_input, expected): ) def test_toEpoch(test_input, expected): """Test time to epoch conversion.""" - assert pyatmo.toEpoch(test_input) == expected + assert pyatmo.helpers.toEpoch(test_input) == expected @pytest.mark.parametrize( @@ -114,4 +114,4 @@ def mockreturn(format): return test_input monkeypatch.setattr(time, "strftime", mockreturn) - assert pyatmo.todayStamps() == expected + assert pyatmo.helpers.todayStamps() == expected diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index d0579ba86..5d6ca5627 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -4,7 +4,7 @@ import pytest from freezegun import freeze_time -import pyatmo.Camera +import pyatmo INVALID_NAME = "InvalidName" @@ -42,7 +42,7 @@ def test_CameraData_homeById(cameraHomeData, hid, expected): ) def test_CameraData_homeByName(cameraHomeData, name, expected): if name == INVALID_NAME: - with pytest.raises(pyatmo.Exceptions.InvalidHome): + with pytest.raises(pyatmo.exceptions.InvalidHome): assert cameraHomeData.homeByName(name) else: assert cameraHomeData.homeByName(name)["id"] == expected @@ -146,11 +146,11 @@ def test_CameraData_cameraUrls_disconnected(auth, requests_mock): with open("fixtures/camera_home_data_disconnected.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Camera._GETHOMEDATA_REQ, + pyatmo.camera._GETHOMEDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - camera_data = pyatmo.Camera.CameraData(auth) + camera_data = pyatmo.CameraData(auth) assert camera_data.cameraUrls() == (None, None) @@ -168,7 +168,7 @@ def test_CameraData_cameraUrls_disconnected(auth, requests_mock): ) def test_CameraData_personsAtHome(cameraHomeData, home, expected): if home == INVALID_NAME: - with pytest.raises(pyatmo.Exceptions.InvalidHome): + with pytest.raises(pyatmo.exceptions.InvalidHome): assert cameraHomeData.personsAtHome(home) else: assert cameraHomeData.personsAtHome(home) == expected @@ -333,5 +333,5 @@ def test_CameraData_getHomeName(cameraHomeData): def test_CameraData_gethomeId(cameraHomeData): assert cameraHomeData.gethomeId() == "91763b24c43d3e344f424e8b" assert cameraHomeData.gethomeId("MYHOME") == "91763b24c43d3e344f424e8b" - with pytest.raises(pyatmo.Camera.InvalidHome): + with pytest.raises(pyatmo.InvalidHome): assert cameraHomeData.gethomeId("InvalidName") diff --git a/tests/test_pyatmo_homecoach.py b/tests/test_pyatmo_homecoach.py index 13423268f..eb360385c 100644 --- a/tests/test_pyatmo_homecoach.py +++ b/tests/test_pyatmo_homecoach.py @@ -3,7 +3,7 @@ import pytest -import pyatmo.HomeCoach +import pyatmo def test_HomeCoachData(homeCoachData): @@ -88,9 +88,9 @@ def test_HomeCoachData_no_devices(auth, requests_mock): with open("fixtures/home_coach_no_devices.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.HomeCoach._GETHOMECOACHDATA_REQ, + pyatmo.home_coach._GETHOMECOACHDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(pyatmo.WeatherStation.NoDevice): - assert pyatmo.HomeCoach.HomeCoachData(auth) + with pytest.raises(pyatmo.NoDevice): + assert pyatmo.home_coach.HomeCoachData(auth) diff --git a/tests/test_pyatmo_publicdata.py b/tests/test_pyatmo_publicdata.py index 8f41044e8..205390cbc 100644 --- a/tests/test_pyatmo_publicdata.py +++ b/tests/test_pyatmo_publicdata.py @@ -3,42 +3,42 @@ import pytest -import pyatmo.PublicData +import pyatmo def test_PublicData(auth, requests_mock): with open("fixtures/public_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.PublicData._GETPUBLIC_DATA, + pyatmo.public_data._GETPUBLIC_DATA, json=json_fixture, headers={"content-type": "application/json"}, ) - publicData = pyatmo.PublicData.PublicData(auth) + publicData = pyatmo.PublicData(auth) assert publicData.status == "ok" - publicData = pyatmo.PublicData.PublicData( + publicData = pyatmo.PublicData( auth, required_data_type="temperature,rain_live" ) assert publicData.status == "ok" def test_PublicData_unavailable(auth, requests_mock): - requests_mock.post(pyatmo.PublicData._GETPUBLIC_DATA, status_code=404) - with pytest.raises(pyatmo.PublicData.NoDevice): - pyatmo.PublicData.PublicData(auth) + requests_mock.post(pyatmo.public_data._GETPUBLIC_DATA, status_code=404) + with pytest.raises(pyatmo.NoDevice): + pyatmo.PublicData(auth) def test_PublicData_error(auth, requests_mock): with open("fixtures/public_data_error_mongo.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.PublicData._GETPUBLIC_DATA, + pyatmo.public_data._GETPUBLIC_DATA, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(pyatmo.PublicData.NoDevice): - pyatmo.PublicData.PublicData(auth) + with pytest.raises(pyatmo.NoDevice): + pyatmo.PublicData(auth) def test_PublicData_CountStationInArea(publicData): @@ -297,4 +297,4 @@ def test_PublicData_getAccessoryMeasures(publicData, test_input, expected): ], ) def test_PublicData_averageMeasure(test_input, expected): - assert pyatmo.PublicData.averageMeasure(test_input) == expected + assert pyatmo.public_data.averageMeasure(test_input) == expected diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index 0c748e40d..a52882426 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -4,8 +4,7 @@ import pytest -import pyatmo.Exceptions -import pyatmo.Thermostat +import pyatmo from .conftest import does_not_raise @@ -65,32 +64,32 @@ def test_HomeData(homeData): def test_HomeData_no_data(auth, requests_mock): - requests_mock.post(pyatmo.Thermostat._GETHOMESDATA_REQ, text="None") - with pytest.raises(pyatmo.Thermostat.NoDevice): - assert pyatmo.Thermostat.HomeData(auth) + requests_mock.post(pyatmo.thermostat._GETHOMESDATA_REQ, text="None") + with pytest.raises(pyatmo.NoDevice): + assert pyatmo.HomeData(auth) def test_HomeData_no_body(auth, requests_mock): with open("fixtures/home_data_empty.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Thermostat._GETHOMESDATA_REQ, + pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(pyatmo.Thermostat.NoDevice): - assert pyatmo.Thermostat.HomeData(auth) + with pytest.raises(pyatmo.NoDevice): + assert pyatmo.HomeData(auth) def test_HomeData_no_home_name(auth, requests_mock): with open("fixtures/home_data_nohomename.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Thermostat._GETHOMESDATA_REQ, + pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - homeData = pyatmo.Thermostat.HomeData(auth) + homeData = pyatmo.HomeData(auth) home_id = "91763b24c43d3e344f424e8b" assert homeData.homeById(home_id)["name"] == "Unknown" @@ -110,7 +109,7 @@ def test_HomeData_homeByName(homeData): def test_HomeData_gethomeId(homeData): assert homeData.gethomeId() == "91763b24c43d3e344f424e8b" assert homeData.gethomeId("MYHOME") == "91763b24c43d3e344f424e8b" - with pytest.raises(pyatmo.Thermostat.InvalidHome): + with pytest.raises(pyatmo.InvalidHome): assert homeData.gethomeId("InvalidName") @@ -125,22 +124,22 @@ def test_HomeData_getHomeName(homeData): def test_HomeData_getSelectedschedule(homeData): assert homeData.getSelectedschedule()["name"] == "Default" assert homeData.getSelectedschedule("MYHOME")["name"] == "Default" - with pytest.raises(pyatmo.Exceptions.InvalidHome): + with pytest.raises(pyatmo.InvalidHome): assert homeData.getSelectedschedule("Unknown") @pytest.mark.parametrize( "t_home, t_sched_id, t_sched, expected", [ - (None, None, None, pytest.raises(pyatmo.Exceptions.NoSchedule)), + (None, None, None, pytest.raises(pyatmo.NoSchedule)), (None, None, "Default", does_not_raise()), (None, "591b54a2764ff4d50d8b5795", None, does_not_raise()), - (None, None, "Summer", pytest.raises(pyatmo.Exceptions.NoSchedule)), + (None, None, "Summer", pytest.raises(pyatmo.NoSchedule)), ( None, "123456789abcdefg12345678", None, - pytest.raises(pyatmo.Exceptions.NoSchedule), + pytest.raises(pyatmo.NoSchedule), ), ], ) @@ -150,7 +149,7 @@ def test_HomeData_switchHomeSchedule( with open("fixtures/status_ok.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Thermostat._SWITCHHOMESCHEDULE_REQ, + pyatmo.thermostat._SWITCHHOMESCHEDULE_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) @@ -180,18 +179,18 @@ def test_HomeStatus_error_and_data(auth, requests_mock): with open("fixtures/home_status_error_and_data.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Thermostat._GETHOMESTATUS_REQ, + pyatmo.thermostat._GETHOMESTATUS_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) with open("fixtures/home_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Thermostat._GETHOMESDATA_REQ, + pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - homeStatus = pyatmo.Thermostat.HomeStatus(auth) + homeStatus = pyatmo.HomeStatus(auth) assert len(homeStatus.rooms) == 3 assert homeStatus.default_room["id"] == "2746182631" @@ -211,19 +210,19 @@ def test_HomeStatus_error(auth, requests_mock): with open("fixtures/home_status_empty.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Thermostat._GETHOMESTATUS_REQ, + pyatmo.thermostat._GETHOMESTATUS_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) with open("fixtures/home_data_simple.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Thermostat._GETHOMESDATA_REQ, + pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(pyatmo.Exceptions.NoDevice): - assert pyatmo.Thermostat.HomeStatus(auth) + with pytest.raises(pyatmo.NoDevice): + assert pyatmo.HomeStatus(auth) def test_HomeStatus_roomById(homeStatus): @@ -237,7 +236,7 @@ def test_HomeStatus_roomById(homeStatus): "therm_setpoint_end_time": 0, } assert homeStatus.roomById("2746182631") == expexted - with pytest.raises(pyatmo.Exceptions.InvalidRoom): + with pytest.raises(pyatmo.InvalidRoom): assert homeStatus.roomById("0000000000") @@ -256,7 +255,7 @@ def test_HomeStatus_thermostatById(homeStatus): "battery_state": "high", } assert homeStatus.thermostatById("12:34:56:00:01:ae") == expexted - with pytest.raises(pyatmo.Exceptions.InvalidRoom): + with pytest.raises(pyatmo.InvalidRoom): assert homeStatus.thermostatById("00:00:00:00:00:00") @@ -269,7 +268,7 @@ def test_HomeStatus_relayById(homeStatus): "wifi_strength": 42, } assert homeStatus.relayById("12:34:56:00:fa:d0") == expexted - with pytest.raises(pyatmo.Exceptions.InvalidRoom): + with pytest.raises(pyatmo.InvalidRoom): assert homeStatus.relayById("00:00:00:00:00:00") @@ -285,14 +284,14 @@ def test_HomeStatus_valveById(homeStatus): "battery_state": "full", } assert homeStatus.valveById("12:34:56:03:a5:54") == expexted - with pytest.raises(pyatmo.Exceptions.InvalidRoom): + with pytest.raises(pyatmo.InvalidRoom): assert homeStatus.valveById("00:00:00:00:00:00") def test_HomeStatus_setPoint(homeStatus): assert homeStatus.setPoint() == 12 assert homeStatus.setPoint("2746182631") == 12 - with pytest.raises(pyatmo.Exceptions.InvalidRoom): + with pytest.raises(pyatmo.InvalidRoom): assert homeStatus.setPoint("0000000000") @@ -313,7 +312,7 @@ def test_HomeStatus_getAwaytemp(homeStatus): def test_HomeStatus_getHgtemp(homeStatus): assert homeStatus.getHgtemp() == 7 assert homeStatus.getHgtemp("MYHOME") == 7 - with pytest.raises(pyatmo.Exceptions.InvalidHome): + with pytest.raises(pyatmo.InvalidHome): assert homeStatus.getHgtemp("InvalidHome") assert homeStatus.getHgtemp(home_id="91763b24c43d3e344f424e8b") == 7 assert homeStatus.getHgtemp(home_id="00000000000000000000000") is None @@ -322,7 +321,7 @@ def test_HomeStatus_getHgtemp(homeStatus): def test_HomeStatus_measuredTemperature(homeStatus): assert homeStatus.measuredTemperature() == 19.8 assert homeStatus.measuredTemperature("2746182631") == 19.8 - with pytest.raises(pyatmo.Exceptions.InvalidRoom): + with pytest.raises(pyatmo.InvalidRoom): assert homeStatus.measuredTemperature("0000000000") @@ -333,7 +332,7 @@ def test_HomeStatus_boilerStatus(homeStatus): def test_HomeStatus_thermostatType(homeStatus): assert homeStatus.thermostatType("MYHOME", "2746182631") == "NATherm1" assert homeStatus.thermostatType("MYHOME", "2833524037") == "NRV" - with pytest.raises(pyatmo.Exceptions.InvalidHome): + with pytest.raises(pyatmo.InvalidHome): assert homeStatus.thermostatType("InvalidHome", "2833524037") assert homeStatus.thermostatType("MYHOME", "0000000000") is None @@ -358,7 +357,7 @@ def test_HomeData_setThermmode( with open("fixtures/%s" % json_fixture) as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Thermostat._SETTHERMMODE_REQ, + pyatmo.thermostat._SETTHERMMODE_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) @@ -395,7 +394,7 @@ def test_HomeData_setroomThermpoint( with open("fixtures/%s" % json_fixture) as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Thermostat._SETROOMTHERMPOINT_REQ, + pyatmo.thermostat._SETROOMTHERMPOINT_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) @@ -458,7 +457,7 @@ def test_HomeData_setroomThermpoint_error( with open("fixtures/%s" % json_fixture) as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.Thermostat._SETROOMTHERMPOINT_REQ, + pyatmo.thermostat._SETROOMTHERMPOINT_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 3c9d5dc94..6dcfcc2bb 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -4,9 +4,7 @@ import pytest from freezegun import freeze_time -import pyatmo.WeatherStation - -from .conftest import does_not_raise +import pyatmo def test_WeatherStationData(weatherStationData): @@ -14,33 +12,33 @@ def test_WeatherStationData(weatherStationData): def test_WeatherStationData_no_response(auth, requests_mock): - requests_mock.post(pyatmo.WeatherStation._GETSTATIONDATA_REQ, text="None") - with pytest.raises(pyatmo.WeatherStation.NoDevice): - assert pyatmo.WeatherStation.WeatherStationData(auth) + requests_mock.post(pyatmo.weather_station._GETSTATIONDATA_REQ, text="None") + with pytest.raises(pyatmo.NoDevice): + assert pyatmo.WeatherStationData(auth) def test_WeatherStationData_no_body(auth, requests_mock): with open("fixtures/status_ok.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.WeatherStation._GETSTATIONDATA_REQ, + pyatmo.weather_station._GETSTATIONDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(pyatmo.WeatherStation.NoDevice): - assert pyatmo.WeatherStation.WeatherStationData(auth) + with pytest.raises(pyatmo.NoDevice): + assert pyatmo.WeatherStationData(auth) def test_WeatherStationData_no_data(auth, requests_mock): with open("fixtures/home_data_empty.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.WeatherStation._GETSTATIONDATA_REQ, + pyatmo.weather_station._GETSTATIONDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(pyatmo.WeatherStation.NoDevice): - assert pyatmo.WeatherStation.WeatherStationData(auth) + with pytest.raises(pyatmo.NoDevice): + assert pyatmo.WeatherStationData(auth) @pytest.mark.parametrize( @@ -519,7 +517,7 @@ def test_WeatherStationData_getMeasure( with open("fixtures/weatherstation_measure.json") as f: json_fixture = json.load(f) requests_mock.post( - pyatmo.WeatherStation._GETMEASURE_REQ, + pyatmo.weather_station._GETMEASURE_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) From 1f6c22ee1ea82a1e1c8d2fed5b907f085b681ad1 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 3 Nov 2019 00:52:32 +0100 Subject: [PATCH 143/333] isort imports --- pyatmo/__init__.py | 7 +++---- pyatmo/auth.py | 3 +-- pyatmo/camera.py | 3 +-- pyatmo/public_data.py | 2 +- pyatmo/thermostat.py | 2 +- pyatmo/weather_station.py | 2 +- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/pyatmo/__init__.py b/pyatmo/__init__.py index de0990e8c..d7feebe12 100644 --- a/pyatmo/__init__.py +++ b/pyatmo/__init__.py @@ -1,11 +1,10 @@ from .auth import ClientAuth -from .exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule -from .weather_station import WeatherStationData -from .thermostat import HomeData, HomeStatus from .camera import CameraData +from .exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule from .home_coach import HomeCoachData from .public_data import PublicData - +from .thermostat import HomeData, HomeStatus +from .weather_station import WeatherStationData __all__ = [ "ClientAuth", diff --git a/pyatmo/auth.py b/pyatmo/auth.py index 5529b849f..d82ca443b 100644 --- a/pyatmo/auth.py +++ b/pyatmo/auth.py @@ -1,8 +1,7 @@ import time -from .helpers import _BASE_URL, postRequest, LOG from .exceptions import NoDevice - +from .helpers import _BASE_URL, LOG, postRequest # Common definitions _AUTH_REQ = _BASE_URL + "oauth2/token" diff --git a/pyatmo/camera.py b/pyatmo/camera.py index dbdc67920..d924c9798 100644 --- a/pyatmo/camera.py +++ b/pyatmo/camera.py @@ -2,9 +2,8 @@ import time from urllib.error import URLError -from .helpers import _BASE_URL, postRequest, LOG from .exceptions import InvalidHome, NoDevice - +from .helpers import _BASE_URL, LOG, postRequest _GETHOMEDATA_REQ = _BASE_URL + "api/gethomedata" _GETCAMERAPICTURE_REQ = _BASE_URL + "api/getcamerapicture" diff --git a/pyatmo/public_data.py b/pyatmo/public_data.py index 040ba0e52..72924008b 100644 --- a/pyatmo/public_data.py +++ b/pyatmo/public_data.py @@ -1,5 +1,5 @@ -from .helpers import _BASE_URL, postRequest, toTimeString from .exceptions import NoDevice +from .helpers import _BASE_URL, postRequest, toTimeString _GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata" _LON_NE = 6.221652 diff --git a/pyatmo/thermostat.py b/pyatmo/thermostat.py index 630da7ee7..57b5c7981 100644 --- a/pyatmo/thermostat.py +++ b/pyatmo/thermostat.py @@ -1,7 +1,7 @@ import logging -from .helpers import _BASE_URL, postRequest from .exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule +from .helpers import _BASE_URL, postRequest LOG = logging.getLogger(__name__) diff --git a/pyatmo/weather_station.py b/pyatmo/weather_station.py index 3455c9d9e..47a1e4c20 100644 --- a/pyatmo/weather_station.py +++ b/pyatmo/weather_station.py @@ -1,8 +1,8 @@ import logging import time -from .helpers import _BASE_URL, postRequest, todayStamps from .exceptions import NoDevice +from .helpers import _BASE_URL, postRequest, todayStamps LOG = logging.getLogger(__name__) From e441b3f1520d76bf4267efd7141cc7b151fbf557 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 3 Nov 2019 00:53:41 +0100 Subject: [PATCH 144/333] Sort public classes --- pyatmo/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyatmo/__init__.py b/pyatmo/__init__.py index d7feebe12..0a54dc155 100644 --- a/pyatmo/__init__.py +++ b/pyatmo/__init__.py @@ -7,15 +7,15 @@ from .weather_station import WeatherStationData __all__ = [ + "CameraData", "ClientAuth", - "WeatherStationData", + "HomeCoachData", "HomeData", "HomeStatus", "InvalidHome", "InvalidRoom", "NoDevice", "NoSchedule", - "CameraData", - "HomeCoachData", "PublicData", + "WeatherStationData", ] From a5043ff2612634325c95ec8fa7e0fbc099c71c02 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 4 Nov 2019 09:43:15 +0100 Subject: [PATCH 145/333] Fix failing tests --- tests/test_pyatmo_camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 5d6ca5627..4b945e7b1 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -239,7 +239,7 @@ def test_CameraData_setPersonsAway( with open("fixtures/%s" % json_fixture) as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Camera._SETPERSONSAWAY_REQ, + pyatmo.camera._SETPERSONSAWAY_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) @@ -272,7 +272,7 @@ def test_CameraData_setPersonsHome( with open("fixtures/%s" % json_fixture) as f: json_fixture = json.load(f) requests_mock.post( - smart_home.Camera._SETPERSONSHOME_REQ, + pyatmo.camera._SETPERSONSHOME_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) From 987b42bd784ba435d354bda742674a35c01612ac Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 4 Nov 2019 11:36:37 +0100 Subject: [PATCH 146/333] Remove stale code --- pyatmo/helpers.py | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/pyatmo/helpers.py b/pyatmo/helpers.py index 41f25b222..78b68bc90 100644 --- a/pyatmo/helpers.py +++ b/pyatmo/helpers.py @@ -40,35 +40,3 @@ def toEpoch(value): def todayStamps(): today = timegm(time.strptime(time.strftime("%Y-%m-%d") + "GMT", "%Y-%m-%d%Z")) return today, today + 3600 * 24 - - -# Global shortcut - - -def getStationMinMaxTH(station=None, module=None): - from pyatmo import ClientAuth - from smart_home.WeatherStation import DeviceList - - authorization = ClientAuth() - devList = DeviceList(authorization) - if not station: - station = devList.default_station - if module: - mname = module - else: - mname = devList.stationByName(station)["module_name"] - lastD = devList.lastData(station) - if mname == "*": - result = {} - for m in lastD.keys(): - if time.time() - lastD[m]["When"] > 3600: - continue - r = devList.MinMaxTH(module=m) - result[m] = (r[0], lastD[m]["Temperature"], r[1]) - else: - if time.time() - lastD[mname]["When"] > 3600: - result = ["-", "-"] - else: - result = [lastD[mname]["Temperature"], lastD[mname]["Humidity"]] - result.extend(devList.MinMaxTH(station, mname)) - return result From 4347cb7ffaa57018f97c689342bb12990f8028ed Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 4 Nov 2019 11:37:12 +0100 Subject: [PATCH 147/333] Remove unused variables --- pyatmo/__main__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyatmo/__main__.py b/pyatmo/__main__.py index c067ca609..66504252b 100644 --- a/pyatmo/__main__.py +++ b/pyatmo/__main__.py @@ -37,19 +37,19 @@ def main(): ) try: - ws = WeatherStationData(authorization) + WeatherStationData(authorization) except NoDevice: if stdout.isatty(): print("pyatmo.py : warning, no weather station available for testing") try: - cam = CameraData(authorization) + CameraData(authorization) except NoDevice: if stdout.isatty(): print("pyatmo.py : warning, no camera available for testing") try: - hd = HomeData(authorization) + HomeData(authorization) except NoDevice: if stdout.isatty(): print("pyatmo.py : warning, no thermostat available for testing") From 1b6d6d754d2967f54f037c5be83d17c42fa0c2f9 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 4 Nov 2019 11:48:06 +0100 Subject: [PATCH 148/333] Cleanup --- pyatmo/helpers.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyatmo/helpers.py b/pyatmo/helpers.py index 78b68bc90..1722689c1 100644 --- a/pyatmo/helpers.py +++ b/pyatmo/helpers.py @@ -7,13 +7,9 @@ LOG = logging.getLogger(__name__) -# Common definitions _BASE_URL = "https://api.netatmo.com/" -# Utilities routines - - def postRequest(url, params=None, timeout=10): resp = requests.post(url, data=params, timeout=timeout) if not resp.ok: From f184480d3753ed08d6b48aee611c3e015409c172 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 4 Nov 2019 12:14:58 +0100 Subject: [PATCH 149/333] Add pre-commit hook config --- .pre-commit-config.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..a927eee42 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +# Note: don't use this config for your own repositories. Instead, see +# "Version control integration" in README.md. +exclude: ^(fixtures/) +repos: +- repo: local + hooks: + - id: black + name: black + language: system + entry: black + types: [python] + - id: flake8 + name: flake8 + language: system + entry: flake8 + types: [python] + - id: mypy + name: mypy + language: system + entry: mypy + types: [python] + exclude: tests/ From b9fea63e9048681955cda555547590b29ad50451 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 4 Nov 2019 13:37:16 +0100 Subject: [PATCH 150/333] update gihub action --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index b90bf7b03..84ff7bf0e 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -37,7 +37,7 @@ jobs: - name: Check with black run: | pip install black - black --check smart_home/ tests/ setup.py + black --check pyatmo/ tests/ setup.py - name: Test with pytest run: | pip install pytest freezegun requests-mock From 784bed06a2683b9e089dc4ad41c585065237aafb Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 4 Nov 2019 13:51:07 +0100 Subject: [PATCH 151/333] Blacken code --- tests/test_pyatmo.py | 1 + tests/test_pyatmo_publicdata.py | 4 +--- tests/test_pyatmo_thermostat.py | 7 +------ 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index 70b68b2be..236cb52ce 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -30,6 +30,7 @@ def test_ClientAuth_invalid(requests_mock): password="PASSWORD", ) + def test_postRequest_json(requests_mock): """Test wrapper for posting requests against the Netatmo API.""" requests_mock.post( diff --git a/tests/test_pyatmo_publicdata.py b/tests/test_pyatmo_publicdata.py index 205390cbc..47d7deb52 100644 --- a/tests/test_pyatmo_publicdata.py +++ b/tests/test_pyatmo_publicdata.py @@ -17,9 +17,7 @@ def test_PublicData(auth, requests_mock): publicData = pyatmo.PublicData(auth) assert publicData.status == "ok" - publicData = pyatmo.PublicData( - auth, required_data_type="temperature,rain_live" - ) + publicData = pyatmo.PublicData(auth, required_data_type="temperature,rain_live") assert publicData.status == "ok" diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index a52882426..1948936ad 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -135,12 +135,7 @@ def test_HomeData_getSelectedschedule(homeData): (None, None, "Default", does_not_raise()), (None, "591b54a2764ff4d50d8b5795", None, does_not_raise()), (None, None, "Summer", pytest.raises(pyatmo.NoSchedule)), - ( - None, - "123456789abcdefg12345678", - None, - pytest.raises(pyatmo.NoSchedule), - ), + (None, "123456789abcdefg12345678", None, pytest.raises(pyatmo.NoSchedule),), ], ) def test_HomeData_switchHomeSchedule( From 4255b75cb814f0efea81ef7e7e4de5abd3c452fc Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 6 Nov 2019 00:23:28 +0100 Subject: [PATCH 152/333] Use src layout and introduce tox --- .gitignore | 3 ++- README.md | 14 ++++++++++- pyproject.toml | 3 +++ setup.cfg | 5 +++- setup.py | 7 +++--- {pyatmo => src/pyatmo}/__init__.py | 0 {pyatmo => src/pyatmo}/__main__.py | 0 {pyatmo => src/pyatmo}/auth.py | 0 {pyatmo => src/pyatmo}/camera.py | 0 {pyatmo => src/pyatmo}/exceptions.py | 0 {pyatmo => src/pyatmo}/helpers.py | 0 {pyatmo => src/pyatmo}/home_coach.py | 0 {pyatmo => src/pyatmo}/public_data.py | 0 {pyatmo => src/pyatmo}/thermostat.py | 0 {pyatmo => src/pyatmo}/weather_station.py | 0 tox.ini | 29 +++++++++++++++++++++++ 16 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 pyproject.toml rename {pyatmo => src/pyatmo}/__init__.py (100%) rename {pyatmo => src/pyatmo}/__main__.py (100%) rename {pyatmo => src/pyatmo}/auth.py (100%) rename {pyatmo => src/pyatmo}/camera.py (100%) rename {pyatmo => src/pyatmo}/exceptions.py (100%) rename {pyatmo => src/pyatmo}/helpers.py (100%) rename {pyatmo => src/pyatmo}/home_coach.py (100%) rename {pyatmo => src/pyatmo}/public_data.py (100%) rename {pyatmo => src/pyatmo}/thermostat.py (100%) rename {pyatmo => src/pyatmo}/weather_station.py (100%) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 25d590d25..2906e186f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ pyatmo.egg-info/ venv/ .mypy_cache/ -.pytest_cache/ \ No newline at end of file +.pytest_cache/ +.tox/ \ No newline at end of file diff --git a/README.md b/README.md index 675b599b2..5191ba74a 100644 --- a/README.md +++ b/README.md @@ -34,14 +34,26 @@ To run the pytest testsuite you need to install the following dependencies: pip install pytest pytest-mock pytest-cov requests-mock freezegun +`pyatmo` has to be installed in editable mode to be picked up by pytest. Simply run: + + pip install -e . + To run the full suite simply type in pytest or - python -m pytest + python -m pytest tests/ To generate code coverage xml (e.g. for use in VSCode) run python -m pytest --cov-report xml:cov.xml --cov smart_home --cov-append tests/ + +Another way to run the tests is by using `tox`. This runs the tests against the installed package and multiple versions of python. + + tox + +or by specifying a python version + + tox -e py38 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..0d8d90c89 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["wheel", "setuptools", "attrs>=17.1"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 581ae06d4..6daec6ffd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,4 +7,7 @@ ignore = W503, E501 [pep8] max-line-length = 88 -ignore = W503, E501 \ No newline at end of file +ignore = W503, E501 + +[mypy] +ignore_errors = True \ No newline at end of file diff --git a/setup.py b/setup.py index c0b4cafc5..a2278d4f0 100755 --- a/setup.py +++ b/setup.py @@ -1,15 +1,14 @@ # python setup.py --dry-run --verbose install -from distutils.core import setup +from setuptools import setup, find_packages setup( name="pyatmo", version="3.0.0", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", - py_modules=["pyatmo"], - packages=["pyatmo"], - package_dir={"pyatmo": "pyatmo"}, + packages=find_packages(exclude=["tests"], where="src"), + package_dir={"": "src"}, scripts=[], data_files=[], url="https://github.com/jabesq/netatmo-api-python", diff --git a/pyatmo/__init__.py b/src/pyatmo/__init__.py similarity index 100% rename from pyatmo/__init__.py rename to src/pyatmo/__init__.py diff --git a/pyatmo/__main__.py b/src/pyatmo/__main__.py similarity index 100% rename from pyatmo/__main__.py rename to src/pyatmo/__main__.py diff --git a/pyatmo/auth.py b/src/pyatmo/auth.py similarity index 100% rename from pyatmo/auth.py rename to src/pyatmo/auth.py diff --git a/pyatmo/camera.py b/src/pyatmo/camera.py similarity index 100% rename from pyatmo/camera.py rename to src/pyatmo/camera.py diff --git a/pyatmo/exceptions.py b/src/pyatmo/exceptions.py similarity index 100% rename from pyatmo/exceptions.py rename to src/pyatmo/exceptions.py diff --git a/pyatmo/helpers.py b/src/pyatmo/helpers.py similarity index 100% rename from pyatmo/helpers.py rename to src/pyatmo/helpers.py diff --git a/pyatmo/home_coach.py b/src/pyatmo/home_coach.py similarity index 100% rename from pyatmo/home_coach.py rename to src/pyatmo/home_coach.py diff --git a/pyatmo/public_data.py b/src/pyatmo/public_data.py similarity index 100% rename from pyatmo/public_data.py rename to src/pyatmo/public_data.py diff --git a/pyatmo/thermostat.py b/src/pyatmo/thermostat.py similarity index 100% rename from pyatmo/thermostat.py rename to src/pyatmo/thermostat.py diff --git a/pyatmo/weather_station.py b/src/pyatmo/weather_station.py similarity index 100% rename from pyatmo/weather_station.py rename to src/pyatmo/weather_station.py diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..ab72da35c --- /dev/null +++ b/tox.ini @@ -0,0 +1,29 @@ +[tox] +envlist = py36,py37,py38,pypy3 +isolated_build = True +skip_missing_interpreters = True + +[testenv] +deps = + pytest + pytest-cov + pytest-mock + requests-mock + freezegun + +commands = + python -m pytest --cov {envsitepackagesdir}/pyatmo + +[coverage:paths] +source = + pyatmo + .tox/*/lib/python*/site-packages/pyatmo + +[coverage:run] +branch = true +source = + pyatmo + +[coverage:report] +show_missing = true +precision = 2 From 82876b81c59dcdb0f58eee89c0238e5a8a478e82 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 6 Nov 2019 00:26:37 +0100 Subject: [PATCH 153/333] Update GH actions to use tox --- .github/workflows/pythonpackage.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 84ff7bf0e..07dffa013 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.6, 3.7] + python-version: [3.6, 3.7, 3.8] steps: - uses: actions/checkout@v1 @@ -38,7 +38,7 @@ jobs: run: | pip install black black --check pyatmo/ tests/ setup.py - - name: Test with pytest + - name: Run tests with tox run: | - pip install pytest freezegun requests-mock - pytest + pip install tox + tox \ No newline at end of file From 8ed4a851f4f6cb9f09e1a162862c46ade497344e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 6 Nov 2019 00:28:08 +0100 Subject: [PATCH 154/333] Update black call in GH action --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 07dffa013..3b72c4c72 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -37,7 +37,7 @@ jobs: - name: Check with black run: | pip install black - black --check pyatmo/ tests/ setup.py + black --check src/pyatmo/ tests/ setup.py - name: Run tests with tox run: | pip install tox From 19bbe610d963791e19893a2142e7c5a0cd9f3753 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 6 Nov 2019 00:34:47 +0100 Subject: [PATCH 155/333] Only run specified python version with tox --- .github/workflows/pythonpackage.yml | 2 +- tox.ini | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 3b72c4c72..df26c8133 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -40,5 +40,5 @@ jobs: black --check src/pyatmo/ tests/ setup.py - name: Run tests with tox run: | - pip install tox + pip install tox tox-gh-actions tox \ No newline at end of file diff --git a/tox.ini b/tox.ini index ab72da35c..d86079d3c 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,12 @@ envlist = py36,py37,py38,pypy3 isolated_build = True skip_missing_interpreters = True +[gh-actions] +python = + 3.6: py36 + 3.7: py37 + 3.8: py38 + [testenv] deps = pytest From b088dda9355eda4530b1642b9f926d1b98df5cab Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 6 Nov 2019 00:47:22 +0100 Subject: [PATCH 156/333] Add isort to pre-commit --- .isort.cfg | 6 ------ .pre-commit-config.yaml | 6 ++++++ setup.cfg | 9 ++++++++- setup.py | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) delete mode 100644 .isort.cfg diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 0ac773da8..000000000 --- a/.isort.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[settings] -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a927eee42..a2d8d2dc7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,3 +20,9 @@ repos: entry: mypy types: [python] exclude: tests/ + - id: isort + name: isort + language: system + entry: isort + types: [python] + exclude: tests/ diff --git a/setup.cfg b/setup.cfg index 6daec6ffd..fb530116f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,4 +10,11 @@ max-line-length = 88 ignore = W503, E501 [mypy] -ignore_errors = True \ No newline at end of file +ignore_errors = True + +[isort] +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True +line_length=88 \ No newline at end of file diff --git a/setup.py b/setup.py index a2278d4f0..d715934ac 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ # python setup.py --dry-run --verbose install -from setuptools import setup, find_packages +from setuptools import find_packages, setup setup( name="pyatmo", From 2ec8ef41ec5ca774ada55fc755729e5691708a01 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 7 Nov 2019 00:15:43 +0100 Subject: [PATCH 157/333] Add documentation about the pre-commit hook --- README.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5191ba74a..a02fda76c 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ netatmo-api-python ================== + [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) [![GitHub Actions](https://github.com/jabesq/netatmo-api-python/workflows/Python%20package/badge.svg)](https://github.com/jabesq/netatmo-api-python/actions?workflow=Python+package) [![PyPi](https://img.shields.io/pypi/v/pyatmo.svg)](https://pypi.python.org/pypi/pyatmo) [![PyPi](https://img.shields.io/pypi/l/pyatmo.svg)](https://github.com/jabesq/netatmo-api-python/blob/master/LICENSE.txt) - Simple API to access Netatmo devices and data like weather station or camera data from Python 3. -For more detailed information see http://dev.netatmo.com +For more detailed information see [dev.netatmo.com](http://dev.netatmo.com) This project has no relation with the Netatmo company. -### Install ### +Install +------- To install pyatmo simply run: @@ -22,13 +23,22 @@ Once installed you can simple add `pyatmo` to your Python 3 scripts by including import pyatmo -### Note ### +Note +---- -The module requires a valid user account and a registered application. See usage.md for further information. +The module requires a valid user account and a registered application. See [usage.md](./usage.md) for further information. Be aware that the module may stop working if Netatmo decides to change their API. +Development +----------- + +To add the pre-commit hook to your environment run: + + pip install pre-commit flake8 mypy black isort + pre-commit install -### Testing ### +Testing +------- To run the pytest testsuite you need to install the following dependencies: From ab75d6b0f6b16ca6dd1ea5b275514316746a37c7 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 7 Nov 2019 10:33:57 +0100 Subject: [PATCH 158/333] Format code --- tests/test_pyatmo_camera.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 4b945e7b1..66025e4b3 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -6,7 +6,6 @@ import pyatmo - INVALID_NAME = "InvalidName" From a54f05d044b57dd2724db9a61c00ffa9be00724c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 7 Nov 2019 10:34:07 +0100 Subject: [PATCH 159/333] Update isort config --- setup.cfg | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index fb530116f..f14dc0021 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,4 +17,9 @@ multi_line_output=3 include_trailing_comma=True force_grid_wrap=0 use_parentheses=True -line_length=88 \ No newline at end of file +sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +forced_separate = tests +combine_as_imports = true +line_length=88 +not_skip = __init__.py +skip_glob = venv \ No newline at end of file From dfef0af04fd39647e13cc9574a84ed928246f716 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 7 Nov 2019 11:25:47 +0100 Subject: [PATCH 160/333] Fix linter complaints --- .pylintrc | 56 +++++++++++++++++++++++++++++++++++ src/pyatmo/__main__.py | 16 +++++----- src/pyatmo/auth.py | 2 +- src/pyatmo/camera.py | 22 ++++++-------- src/pyatmo/public_data.py | 15 +++++----- src/pyatmo/thermostat.py | 17 ++++------- src/pyatmo/weather_station.py | 15 +++++----- 7 files changed, 94 insertions(+), 49 deletions(-) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 000000000..434c83000 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,56 @@ +[MESSAGES CONTROL] +# Reasons disabled: +# locally-disabled - it spams too much +# duplicate-code - unavoidable +# cyclic-import - doesn't test if both import on load +# abstract-class-little-used - prevents from setting right foundation +# unused-argument - generic callbacks and setup methods create a lot of warnings +# global-statement - used for the on-demand requirement installation +# redefined-variable-type - this is Python, we're duck typing! +# too-many-* - are not enforced for the sake of readability +# too-few-* - same as too-many-* +# abstract-method - with intro of async there are always methods missing +# inconsistent-return-statements - doesn't handle raise +# not-an-iterable - https://github.com/PyCQA/pylint/issues/2311 +# unnecessary-pass - readability for functions which only contain pass +disable= + abstract-class-little-used, + abstract-method, + cyclic-import, + duplicate-code, + global-statement, + inconsistent-return-statements, + invalid-name, + line-too-long, + locally-disabled, + missing-docstring, + not-an-iterable, + not-context-manager, + redefined-variable-type, + too-few-public-methods, + too-many-arguments, + too-many-branches, + too-many-instance-attributes, + too-many-lines, + too-many-locals, + too-many-public-methods, + too-many-return-statements, + too-many-statements, + unnecessary-pass, + unused-argument, + C0330, + W1203 + +[REPORTS] +reports=no + +[TYPECHECK] +# For attrs +ignored-classes=_CountingAttr +generated-members=botocore.errorfactory + +[FORMAT] +expected-line-ending-format=LF + +[EXCEPTIONS] +overgeneral-exceptions=Exception diff --git a/src/pyatmo/__main__.py b/src/pyatmo/__main__.py index 66504252b..5ce3605f9 100644 --- a/src/pyatmo/__main__.py +++ b/src/pyatmo/__main__.py @@ -7,7 +7,7 @@ def main(): - from sys import exit, stdout, stderr + import sys try: import os @@ -23,10 +23,10 @@ def main(): USERNAME = os.environ["USERNAME"] PASSWORD = os.environ["PASSWORD"] except KeyError: - stderr.write( + sys.stderr.write( "No credentials passed to pyatmo.py (client_id, client_secret, username, password)\n" ) - exit(1) + sys.exit(1) authorization = ClientAuth( clientId=CLIENT_ID, @@ -39,19 +39,19 @@ def main(): try: WeatherStationData(authorization) except NoDevice: - if stdout.isatty(): + if sys.stdout.isatty(): print("pyatmo.py : warning, no weather station available for testing") try: CameraData(authorization) except NoDevice: - if stdout.isatty(): + if sys.stdout.isatty(): print("pyatmo.py : warning, no camera available for testing") try: HomeData(authorization) except NoDevice: - if stdout.isatty(): + if sys.stdout.isatty(): print("pyatmo.py : warning, no thermostat available for testing") PublicData(authorization) @@ -59,10 +59,10 @@ def main(): # If we reach this line, all is OK # If launched interactively, display OK message - if stdout.isatty(): + if sys.stdout.isatty(): print("pyatmo: OK") - exit(0) + sys.exit(0) if __name__ == "__main__": diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index d82ca443b..599cde866 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -46,7 +46,7 @@ def __init__( self._clientSecret = clientSecret try: self._accessToken = resp["access_token"] - except (KeyError): + except KeyError: LOG.error("Netatmo API returned %s", resp["error"]) raise NoDevice("Authentication against Netatmo API failed") self.refreshToken = resp["refresh_token"] diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index d924c9798..6a3145a1e 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -91,10 +91,7 @@ def __init__(self, authData, size=15): self.default_module = list(self.modules.values())[0]["name"] else: self.default_module = None - if ( - self.default_home is not None - and len(self.cameras[self.default_home_id]) > 0 - ): + if self.default_home is not None and self.cameras[self.default_home_id]: self.default_camera = list(self.cameras[self.default_home_id].values())[0] def homeById(self, hid): @@ -146,7 +143,7 @@ def cameraByName(self, camera=None, home=None, home_id=None): hid = home_id if camera is None and home is None and home_id is None: return self.default_camera - elif not (home_id or home) and camera: + if not (home_id or home) and camera: for h_id, cam_ids in self.cameras.items(): for cam_id in cam_ids: if self.cameras[h_id][cam_id]["name"] == camera: @@ -168,8 +165,7 @@ def moduleByName(self, module=None, camera=None, home=None): if not module: if self.default_module: return self.moduleByName(self.default_module) - else: - return None + return None cam = None if camera or home: cam = self.cameraByName(camera, home) @@ -373,7 +369,7 @@ def personSeenByCamera(self, name, home=None, camera=None, exclude=0): for time_ev in array_time_event: if time_ev < limit: return False - elif self.events[cam_id][time_ev]["type"] == "person": + if self.events[cam_id][time_ev]["type"] == "person": person_id = self.events[cam_id][time_ev]["person_id"] if "pseudo" in self.persons[person_id]: if self.persons[person_id]["pseudo"] == name: @@ -418,7 +414,7 @@ def someoneKnownSeen(self, home=None, camera=None, exclude=0, cid=None): for time_ev in array_time_event: if time_ev < limit: return False - elif self.events[cid][time_ev]["type"] == "person": + if self.events[cid][time_ev]["type"] == "person": if self.events[cid][time_ev]["person_id"] in self._knownPersons(): return True # Check in the last event if someone known has been seen @@ -444,7 +440,7 @@ def someoneUnknownSeen(self, home=None, camera=None, exclude=0, cid=None): for time_ev in array_time_event: if time_ev < limit: return False - elif self.events[cid][time_ev]["type"] == "person": + if self.events[cid][time_ev]["type"] == "person": if ( self.events[cid][time_ev]["person_id"] not in self._knownPersons() @@ -473,7 +469,7 @@ def motionDetected(self, home=None, camera=None, exclude=0, cid=None): for time_ev in array_time_event: if time_ev < limit: return False - elif self.events[cid][time_ev]["type"] == "movement": + if self.events[cid][time_ev]["type"] == "movement": return True elif self.lastEvent[cid]["type"] == "movement": return True @@ -571,7 +567,7 @@ def moduleMotionDetected(self, module=None, home=None, camera=None, exclude=0): for time_ev in array_time_event: if time_ev < limit: return False - elif ( + if ( self.events[cam_id][time_ev]["type"] == "tag_big_move" or self.events[cam_id][time_ev]["type"] == "tag_small_move" ) and self.events[cam_id][time_ev]["module_id"] == mod_id: @@ -601,7 +597,7 @@ def moduleOpened(self, module=None, home=None, camera=None, exclude=0): for time_ev in array_time_event: if time_ev < limit: return False - elif ( + if ( self.events[cam_id][time_ev]["type"] == "tag_open" and self.events[cam_id][time_ev]["module_id"] == mod_id ): diff --git a/src/pyatmo/public_data.py b/src/pyatmo/public_data.py index 72924008b..e7e2025ea 100644 --- a/src/pyatmo/public_data.py +++ b/src/pyatmo/public_data.py @@ -140,34 +140,33 @@ def getTimeForRainMeasures(self): def getTimeForWindMeasures(self): return self.getAccessoryMeasures(_ACCESSORY_WIND_TIME_TYPE) - def getLatestStationMeasures(self, type): + def getLatestStationMeasures(self, data_type): measures = {} for station in self.raw_data: for _, module in station["measures"].items(): if ( "type" in module - and type in module["type"] + and data_type in module["type"] and "res" in module and module["res"] ): - measure_index = module["type"].index(type) + measure_index = module["type"].index(data_type) latest_timestamp = sorted(module["res"], reverse=True)[0] measures[station["_id"]] = module["res"][latest_timestamp][ measure_index ] return measures - def getAccessoryMeasures(self, type): + def getAccessoryMeasures(self, data_type): measures = {} for station in self.raw_data: for _, module in station["measures"].items(): - if type in module: - measures[station["_id"]] = module[type] + if data_type in module: + measures[station["_id"]] = module[data_type] return measures def averageMeasure(measures): if measures: return sum(measures.values()) / len(measures) - else: - return 0.0 + return 0.0 diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 57b5c7981..77da36d18 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -118,13 +118,13 @@ def getSelectedschedule(self, home=None, home_id=None): home_id = self.gethomeId(home=home) try: - self.schedule = self.schedules[home_id] + schedule = self.schedules[home_id] except KeyError: raise NoSchedule("No schedules available for %s" % home_id) - for key in self.schedule.keys(): - if "selected" in self.schedule[key].keys(): - return self.schedule[key] + for key in schedule.keys(): + if "selected" in schedule[key].keys(): + return schedule[key] def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): if home is None: @@ -152,17 +152,14 @@ def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): LOG.debug("Response: %s", resp) -class HomeStatus(HomeData): - """ - """ - +class HomeStatus: def __init__(self, authData, home_id=None, home=None): self.getAuthToken = authData.accessToken self.home_data = HomeData(authData) if home_id is not None: self.home_id = home_id - LOG.debug("home_id", self.home_id) + LOG.debug("home_id: %s", self.home_id) elif home is not None: self.home_id = self.home_data.gethomeId(home=home) else: @@ -172,7 +169,6 @@ def __init__(self, authData, home_id=None, home=None): resp = postRequest(_GETHOMESTATUS_REQ, postParams) if "errors" in resp or "body" not in resp or "home" not in resp["body"]: raise NoDevice("No device found, errors in response") - return None self.rawData = resp["body"]["home"] self.rooms = {} self.thermostats = {} @@ -321,7 +317,6 @@ def thermostatType(self, home, rid, home_id=None): for key in self.home_data.rooms[home_id]: if key == rid: for module_id in self.home_data.rooms[home_id][rid]["module_ids"]: - self.module_id = module_id if module_id in self.thermostats: return "NATherm1" if module_id in self.valves: diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 47a1e4c20..b16c648d2 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -59,8 +59,8 @@ def modulesNamesList(self, station=None, station_id=None): res.add(m["module_name"]) else: res.update([m["module_name"] for m in self.modules.values()]) - for _, station in self.stations.items(): - res.add(station["module_name"]) + for s in self.stations.values(): + res.add(s["module_name"]) return list(res) def getModules(self, station=None, station_id=None): @@ -107,12 +107,12 @@ def moduleByName(self, module, station=None): s = self.stationByName(station) if not s: return None - elif s["module_name"] == module: + if s["module_name"] == module: return s else: - for _, station in self.stations.items(): - if "module_name" in station and station["module_name"] == module: - return station + for s in self.stations.values(): + if "module_name" in s and s["module_name"] == module: + return s for m in self.modules: mod = self.modules[m] if mod["module_name"] == module: @@ -295,5 +295,4 @@ def MinMaxTH(self, station=None, module=None, frame="last24"): T = [v[0] for v in resp["body"].values()] H = [v[1] for v in resp["body"].values()] return min(T), max(T), min(H), max(H) - else: - return None + return None From cd32f2f3d41bba5ace090401516a4e62e9cc03b9 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 7 Nov 2019 12:28:58 +0100 Subject: [PATCH 161/333] Split GitHub Actions jobs --- .github/workflows/pythonpackage.yml | 51 ++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index df26c8133..1a10eac01 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -9,13 +9,12 @@ on: - master jobs: - build: - + black: runs-on: ubuntu-latest strategy: - max-parallel: 4 + max-parallel: 1 matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.6] steps: - uses: actions/checkout@v1 @@ -23,10 +22,24 @@ jobs: uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Check with black run: | - python -m pip install --upgrade pip - pip install -e . + pip install black + black --check src/pyatmo/ tests/ setup.py + + linter: + runs-on: ubuntu-latest + strategy: + max-parallel: 1 + matrix: + python-version: [3.6] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} - name: Lint with flake8 run: | pip install flake8 @@ -34,11 +47,27 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Check with black + + build: + + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: [3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | - pip install black - black --check src/pyatmo/ tests/ setup.py + python -m pip install --upgrade pip + pip install -e . - name: Run tests with tox run: | pip install tox tox-gh-actions - tox \ No newline at end of file + tox + \ No newline at end of file From 634ddba52b9c844d3eb614f4119869509cd7e566 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 7 Nov 2019 12:48:36 +0100 Subject: [PATCH 162/333] Set default timeout to 30 seconds --- src/pyatmo/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/helpers.py b/src/pyatmo/helpers.py index 1722689c1..87c4f066f 100644 --- a/src/pyatmo/helpers.py +++ b/src/pyatmo/helpers.py @@ -10,7 +10,7 @@ _BASE_URL = "https://api.netatmo.com/" -def postRequest(url, params=None, timeout=10): +def postRequest(url, params=None, timeout=30): resp = requests.post(url, data=params, timeout=timeout) if not resp.ok: LOG.error("The Netatmo API returned %s", resp.status_code) From 8cd72f79f41934e666c9f425b20a5ce4df40b6f0 Mon Sep 17 00:00:00 2001 From: Hugo D Date: Thu, 7 Nov 2019 12:21:44 +0100 Subject: [PATCH 163/333] Add python and pyCharm gitignore.io specs Signed-off-by: Hugo D --- .gitignore | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 199 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e1473d8d1..9b0b9fb40 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,202 @@ pyatmo.egg-info/ *.pyc .DS_Store -venv/ \ No newline at end of file +venv/ +# Created by https://www.gitignore.io/api/python,pycharm +# Edit at https://www.gitignore.io/?templates=python,pycharm + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/**/sonarlint/ + +# SonarQube Plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator/ + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/python,pycharm +.venv From ca1c95981de48aa0028a829f2e840a2d9c6c77ee Mon Sep 17 00:00:00 2001 From: Hugo D Date: Thu, 7 Nov 2019 14:11:00 +0100 Subject: [PATCH 164/333] v2.3.3 Signed-off-by: Hugo D --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 15b03eba5..2274ae304 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pyatmo", - version="2.3.2", # Should be updated with new versions + version="2.3.3", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", py_modules=["pyatmo"], From 36ad082009fb7486771b319a63ac211f3b4069da Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 8 Nov 2019 10:12:43 +0100 Subject: [PATCH 165/333] Update .gitignore --- .gitignore | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9b0b9fb40..f36d7d0d0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ pyatmo.egg-info/ .DS_Store venv/ +.venv + # Created by https://www.gitignore.io/api/python,pycharm # Edit at https://www.gitignore.io/?templates=python,pycharm @@ -206,4 +208,15 @@ dmypy.json .pyre/ # End of https://www.gitignore.io/api/python,pycharm -.venv + +# Created by https://www.gitignore.io/api/code +# Edit at https://www.gitignore.io/?templates=code + +### Code ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# End of https://www.gitignore.io/api/code From e601f9e407b5289f9b85fad22354dd0e49475d65 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 11 Nov 2019 09:42:26 +0100 Subject: [PATCH 166/333] Incorporate pipenv instructions --- .github/workflows/pythonpackage.yml | 1 - Pipfile | 2 ++ README.md | 18 ++++++++---------- pyproject.toml | 2 +- setup.cfg | 2 +- tests/test_pyatmo_thermostat.py | 2 +- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 1a10eac01..4290f7ed1 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -70,4 +70,3 @@ jobs: run: | pip install tox tox-gh-actions tox - \ No newline at end of file diff --git a/Pipfile b/Pipfile index b9ee2fda7..cfa438b3a 100644 --- a/Pipfile +++ b/Pipfile @@ -15,6 +15,8 @@ pytest-mock = "*" pytest-cov = "*" requests-mock = "*" freezegun = "*" +isort = "*" +tox = "*" [packages] pyatmo = {editable = true,path = "."} diff --git a/README.md b/README.md index a02fda76c..3cd8ac1a1 100644 --- a/README.md +++ b/README.md @@ -32,23 +32,21 @@ Be aware that the module may stop working if Netatmo decides to change their API Development ----------- +Clone the repo and install dependencies: + + git clone + cd netatmo-api-python + pipenv install --dev + To add the pre-commit hook to your environment run: - pip install pre-commit flake8 mypy black isort + pip install pre-commit pre-commit install Testing ------- -To run the pytest testsuite you need to install the following dependencies: - - pip install pytest pytest-mock pytest-cov requests-mock freezegun - -`pyatmo` has to be installed in editable mode to be picked up by pytest. Simply run: - - pip install -e . - -To run the full suite simply type in +To run the full suite simply run the following command from within the virtual environment: pytest diff --git a/pyproject.toml b/pyproject.toml index 0d8d90c89..eb1952f1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] requires = ["wheel", "setuptools", "attrs>=17.1"] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index f14dc0021..8f76832a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,4 +22,4 @@ forced_separate = tests combine_as_imports = true line_length=88 not_skip = __init__.py -skip_glob = venv \ No newline at end of file +skip_glob = venv diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index 1948936ad..115391dab 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -135,7 +135,7 @@ def test_HomeData_getSelectedschedule(homeData): (None, None, "Default", does_not_raise()), (None, "591b54a2764ff4d50d8b5795", None, does_not_raise()), (None, None, "Summer", pytest.raises(pyatmo.NoSchedule)), - (None, "123456789abcdefg12345678", None, pytest.raises(pyatmo.NoSchedule),), + (None, "123456789abcdefg12345678", None, pytest.raises(pyatmo.NoSchedule)), ], ) def test_HomeData_switchHomeSchedule( From 0f6bc8d81cb245051f716f62509cd3018f920127 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 11 Nov 2019 09:57:19 +0100 Subject: [PATCH 167/333] Make pre-commit use tools from the venv --- .pre-commit-config.yaml | 47 +++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a2d8d2dc7..6cea6b070 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,28 +1,29 @@ # Note: don't use this config for your own repositories. Instead, see # "Version control integration" in README.md. +default_stages: [commit, push] exclude: ^(fixtures/) repos: -- repo: local + - repo: local hooks: - - id: black - name: black - language: system - entry: black - types: [python] - - id: flake8 - name: flake8 - language: system - entry: flake8 - types: [python] - - id: mypy - name: mypy - language: system - entry: mypy - types: [python] - exclude: tests/ - - id: isort - name: isort - language: system - entry: isort - types: [python] - exclude: tests/ + - id: black + name: black + language: system + entry: pipenv run black --version + types: [python] + - id: flake8 + name: flake8 + language: system + entry: pipenv run flake8 + types: [python] + - id: mypy + name: mypy + language: system + entry: pipenv run mypy + types: [python] + exclude: tests/ + - id: isort + name: isort + language: system + entry: pipenv run isort + types: [python] + exclude: tests/ From 7fd15a253dab7c88b9e10c1ba8ab98784e803b15 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 11 Nov 2019 13:16:44 +0100 Subject: [PATCH 168/333] Fix wrong test --- src/pyatmo/weather_station.py | 7 ++----- tests/test_pyatmo_weatherstation.py | 19 ++++--------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index b16c648d2..6ac5bfcf5 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -170,10 +170,7 @@ def monitoredConditions(self, module=None, moduleId=None): return conditions def lastData(self, station=None, exclude=0, byId=False): - if byId: - key = "_id" - else: - key = "module_name" + key = "_id" if byId else "module_name" if station is not None: stations = [station] else: @@ -181,7 +178,7 @@ def lastData(self, station=None, exclude=0, byId=False): # Breaking change from Netatmo : dashboard_data no longer available if station lost lastD = {} for st in stations: - s = self.stationByName(st) + s = self.stationById(st) if byId else self.stationByName(st) if not s or "dashboard_data" not in s: return None # Define oldest acceptable sensor measure event diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 6dcfcc2bb..4ee42c8e7 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -358,19 +358,7 @@ def test_WeatherStationData_lastData(weatherStationData, station, exclude, expec "station, exclude, expected", [ ( - "MyStation", - None, - [ - "12:34:56:03:1b:e4", - "12:34:56:05:51:20", - "12:34:56:07:bb:0e", - "12:34:56:07:bb:3e", - "12:34:56:36:fc:de", - "12:34:56:37:11:ca", - ], - ), - ( - "", + "12:34:56:37:11:ca", None, [ "12:34:56:03:1b:e4", @@ -381,9 +369,10 @@ def test_WeatherStationData_lastData(weatherStationData, station, exclude, expec "12:34:56:37:11:ca", ], ), + ("", None, None,), ("NoValidStation", None, None), ( - None, + "12:34:56:37:11:ca", 1000000, [ "12:34:56:03:1b:e4", @@ -395,7 +384,7 @@ def test_WeatherStationData_lastData(weatherStationData, station, exclude, expec ], ), ( - None, + "12:34:56:37:11:ca", 798103, [ "12:34:56:03:1b:e4", From 58a32363403ec75b656ee9e00f54f5cc11160e0d Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 11 Nov 2019 20:38:05 +0100 Subject: [PATCH 169/333] That was not meant to be checked in --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6cea6b070..8c3d5e36c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: black name: black language: system - entry: pipenv run black --version + entry: pipenv run black types: [python] - id: flake8 name: flake8 From 4e704df2d2b8353cdc73da7ffe10b7e41bb18b1f Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sun, 17 Nov 2019 20:54:54 +0100 Subject: [PATCH 170/333] Fix lastData issue (#91) * Fix lastDate issue and update test * Update test --- src/pyatmo/weather_station.py | 4 +++- tests/test_pyatmo_weatherstation.py | 26 ++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 6ac5bfcf5..72db9fe4f 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -173,6 +173,8 @@ def lastData(self, station=None, exclude=0, byId=False): key = "_id" if byId else "module_name" if station is not None: stations = [station] + elif byId: + stations = [s["_id"] for s in list(self.stations.values())] else: stations = [s["station_name"] for s in list(self.stations.values())] # Breaking change from Netatmo : dashboard_data no longer available if station lost @@ -184,7 +186,7 @@ def lastData(self, station=None, exclude=0, byId=False): # Define oldest acceptable sensor measure event limit = (time.time() - exclude) if exclude else 0 ds = s["dashboard_data"] - if "module_name" in s and ds["time_utc"] > limit: + if key in s and ds["time_utc"] > limit: lastD[s[key]] = ds.copy() lastD[s[key]]["When"] = lastD[s[key]].pop("time_utc") lastD[s[key]]["wifi_status"] = s["wifi_status"] diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 4ee42c8e7..ac998ebf4 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -299,11 +299,12 @@ def test_WeatherStationData_monitoredConditions(weatherStationData, module, expe @freeze_time("2019-06-11") @pytest.mark.parametrize( - "station, exclude, expected", + "station, exclude, byId, expected", [ ( "MyStation", None, + False, [ "Garden", "Kitchen", @@ -316,6 +317,7 @@ def test_WeatherStationData_monitoredConditions(weatherStationData, module, expe ( "", None, + False, [ "Garden", "Kitchen", @@ -325,10 +327,11 @@ def test_WeatherStationData_monitoredConditions(weatherStationData, module, expe "Yard", ], ), - ("NoValidStation", None, None), + ("NoValidStation", None, False, None), ( None, 1000000, + False, [ "Garden", "Kitchen", @@ -341,12 +344,27 @@ def test_WeatherStationData_monitoredConditions(weatherStationData, module, expe ( None, 798103, + False, ["Garden", "Kitchen", "NetatmoIndoor", "NetatmoOutdoor", "Yard"], ), + ( + None, + 798103, + True, + [ + "12:34:56:03:1b:e4", + "12:34:56:05:51:20", + "12:34:56:07:bb:3e", + "12:34:56:36:fc:de", + "12:34:56:37:11:ca", + ], + ), ], ) -def test_WeatherStationData_lastData(weatherStationData, station, exclude, expected): - mod = weatherStationData.lastData(station, exclude) +def test_WeatherStationData_lastData( + weatherStationData, station, exclude, byId, expected +): + mod = weatherStationData.lastData(station=station, exclude=exclude, byId=byId) if mod: assert sorted(mod) == expected else: From d5bf1ebd311d5b3648b8781c1b0bcd95bda03900 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Fri, 15 Nov 2019 21:46:01 +0100 Subject: [PATCH 171/333] v3.0.1 Signed-off-by: Hugo D. (jabesq) --- Pipfile | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index cfa438b3a..878d5d865 100644 --- a/Pipfile +++ b/Pipfile @@ -17,6 +17,7 @@ requests-mock = "*" freezegun = "*" isort = "*" tox = "*" +pre-commit = "*" [packages] pyatmo = {editable = true,path = "."} diff --git a/setup.py b/setup.py index d715934ac..62601edef 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="pyatmo", - version="3.0.0", # Should be updated with new versions + version="3.0.1", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", packages=find_packages(exclude=["tests"], where="src"), From 14260786cccfbfe083af17d057f79c9eae016a43 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 20 Nov 2019 21:16:05 +0100 Subject: [PATCH 172/333] Add stations conditions to monitored conditions (#93) * Add smoke detector support * Clean up debug messages * Fix missing sensors issue and update tests * Remove print * Remove print * Revert "Add smoke detector support" This reverts commit 007c4cfbac07490d47167440a1b9e714b5fb9405. * Add doc strings --- src/pyatmo/camera.py | 6 +--- src/pyatmo/thermostat.py | 10 +----- src/pyatmo/weather_station.py | 18 +++++++++-- tests/test_pyatmo_thermostat.py | 9 +++--- tests/test_pyatmo_weatherstation.py | 49 ++++++++++++++++++++++------- 5 files changed, 61 insertions(+), 31 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 6a3145a1e..b86c224c1 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -118,8 +118,6 @@ def gethomeId(self, home=None): home = self.default_home for key, value in self.homes.items(): if value["name"] == home: - LOG.debug(self.homes[key]["id"]) - LOG.debug(self.default_home) return self.homes[key]["id"] raise InvalidHome("Invalid Home %s" % home) @@ -250,7 +248,6 @@ def setPersonsHome(self, person_ids, home_id): "person_ids[]": person_ids, } resp = postRequest(_SETPERSONSHOME_REQ, postParams) - LOG.debug(resp) return resp def setPersonsAway(self, person_id, home_id): @@ -263,7 +260,6 @@ def setPersonsAway(self, person_id, home_id): "person_id": person_id, } resp = postRequest(_SETPERSONSAWAY_REQ, postParams) - LOG.debug(resp) return resp def getPersonId(self, name): @@ -305,7 +301,7 @@ def updateEvent(self, event=None, home=None, cameratype=None, home_id=None): try: home_id = self.gethomeId(home) except InvalidHome: - LOG.debug("No valid Home %s", home) + LOG.debug("Invalid Home %s", home) return None if cameratype == "NACamera": # for the Welcome camera diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 77da36d18..5f2ac9b05 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -97,8 +97,6 @@ def gethomeId(self, home=None): home = self.default_home for key, value in self.homes.items(): if value["name"] == home: - LOG.debug(self.homes[key]["id"]) - LOG.debug(self.default_home) if "therm_schedules" in self.homes[key]: return self.homes[key]["id"] raise InvalidHome("Invalid Home %s" % home) @@ -198,7 +196,6 @@ def __init__(self, authData, home_id=None, home=None): self.default_relay = list(self.relays.values())[0] if self.thermostats != {}: self.default_thermostat = list(self.thermostats.values())[0] - LOG.debug(self.thermostats) if self.valves != {}: self.default_valve = list(self.valves.values())[0] @@ -262,7 +259,6 @@ def getAwaytemp(self, home=None, home_id=None): if not home_id: if not home: home = self.home_data.default_home - LOG.debug(self.home_data.default_home) try: home_id = self.home_data.gethomeId(home) except InvalidHome: @@ -279,7 +275,6 @@ def getHgtemp(self, home=None, home_id=None): if not home_id: if not home: home = self.home_data.default_home - LOG.debug(self.home_data.default_home) home_id = self.home_data.gethomeId(home) try: data = self.home_data.getSelectedschedule(home_id=home_id) @@ -293,7 +288,6 @@ def measuredTemperature(self, rid=None): Return the measured temperature of a given room. """ temperature = None - LOG.debug(rid) room_data = self.roomById(rid=rid) if room_data: temperature = room_data["therm_measured_temperature"] @@ -301,7 +295,6 @@ def measuredTemperature(self, rid=None): def boilerStatus(self, rid=None): boiler_status = None - LOG.debug(rid) if rid: relay_status = self.thermostatById(rid=rid) else: @@ -328,8 +321,7 @@ def setThermmode(self, home_id, mode): "home_id": home_id, "mode": mode, } - resp = postRequest(_SETTHERMMODE_REQ, postParams) - LOG.debug(resp) + return postRequest(_SETTHERMMODE_REQ, postParams) def setroomThermpoint(self, home_id, room_id, mode, temp=None): postParams = { diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 72db9fe4f..1039d22d6 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -18,6 +18,7 @@ class WeatherStationData: """ def __init__(self, authData, urlReq=None): + """Initialize the weather station class.""" self.urlReq = urlReq or _GETSTATIONDATA_REQ self.getAuthToken = authData.accessToken postParams = {"access_token": self.getAuthToken} @@ -47,6 +48,7 @@ def __init__(self, authData, urlReq=None): self.default_station = list(self.stations.values())[0]["station_name"] def modulesNamesList(self, station=None, station_id=None): + """Return a list of all modules for a given or all stations.""" res = set() station_data = None if station_id is not None: @@ -64,7 +66,7 @@ def modulesNamesList(self, station=None, station_id=None): return list(res) def getModules(self, station=None, station_id=None): - """Return a dict for Home Assistant to consume.""" + """Return a dict or modules for a given or all stations.""" res = {} station_data = None if station_id is not None: @@ -91,6 +93,7 @@ def getModules(self, station=None, station_id=None): return res def stationByName(self, station=None): + """Return station by name.""" if not station: station = self.default_station for i, s in self.stations.items(): @@ -99,9 +102,11 @@ def stationByName(self, station=None): return None def stationById(self, sid): + """Return station by id.""" return None if sid not in self.stations else self.stations[sid] def moduleByName(self, module, station=None): + """Return module by name.""" s = None if station: s = self.stationByName(station) @@ -121,6 +126,7 @@ def moduleByName(self, module, station=None): return None def moduleById(self, mid, sid=None): + """Return module by id.""" s = self.stationById(sid) if sid else None if mid in self.modules: if s: @@ -131,10 +137,15 @@ def moduleById(self, mid, sid=None): return self.modules[mid] def monitoredConditions(self, module=None, moduleId=None): + """Return monitored conditions for given module(s).""" if moduleId: mod = self.moduleById(moduleId) + if not mod: + mod = self.stationById(moduleId) elif module: mod = self.moduleByName(module) + if not mod: + mod = self.stationByName(module) else: return None conditions = [] @@ -149,7 +160,7 @@ def monitoredConditions(self, module=None, moduleId=None): elif cond == "Rain": conditions.extend(["Rain", "sum_rain_24", "sum_rain_1"]) else: - conditions.append(cond.lower()) + conditions.append(cond) if mod["type"] in ["NAMain", "NHC"]: # the main module has wifi_status conditions.append("wifi_status") @@ -170,6 +181,7 @@ def monitoredConditions(self, module=None, moduleId=None): return conditions def lastData(self, station=None, exclude=0, byId=False): + """Return data for a given station and time frame.""" key = "_id" if byId else "module_name" if station is not None: stations = [station] @@ -211,6 +223,7 @@ def lastData(self, station=None, exclude=0, byId=False): return lastD def checkNotUpdated(self, station=None, delay=3600): + """Check if a given station has not been updated.""" res = self.lastData(station) ret = [] for mn, v in res.items(): @@ -219,6 +232,7 @@ def checkNotUpdated(self, station=None, delay=3600): return ret if ret else None def checkUpdated(self, station=None, delay=3600): + """Check if a given station has been updated.""" res = self.lastData(station) ret = [] for mn, v in res.items(): diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index 115391dab..de930a29a 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -1,6 +1,5 @@ """Define tests for Thermostat module.""" import json -import logging import pytest @@ -356,9 +355,11 @@ def test_HomeData_setThermmode( json=json_fixture, headers={"content-type": "application/json"}, ) - with caplog.at_level(logging.DEBUG): - homeStatus.setThermmode(home_id=home_id, mode=mode) - assert expected in caplog.text + res = homeStatus.setThermmode(home_id=home_id, mode=mode) + if "error" in res: + assert expected in res["error"]["message"] + else: + assert expected in res["status"] @pytest.mark.parametrize( diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index ac998ebf4..507293117 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -221,24 +221,41 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): @pytest.mark.parametrize( - "module, expected", + "module, moduleId, expected", [ + ( + None, + "12:34:56:07:bb:3e", + [ + "CO2", + "Humidity", + "Temperature", + "battery_percent", + "battery_vp", + "max_temp", + "min_temp", + "reachable", + "rf_status", + ], + ), ( "Kitchen", + None, [ + "CO2", + "Humidity", + "Temperature", "battery_percent", "battery_vp", - "co2", - "humidity", "max_temp", "min_temp", "reachable", "rf_status", - "temperature", ], ), ( "Garden", + None, [ "battery_percent", "battery_vp", @@ -252,6 +269,7 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): ), ( "Yard", + None, [ "Rain", "battery_percent", @@ -264,37 +282,46 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): ), ( "NetatmoIndoor", + None, [ - "co2", - "humidity", + "CO2", + "Humidity", + "Noise", + "Pressure", + "Temperature", "max_temp", "min_temp", - "noise", - "pressure", "reachable", - "temperature", "wifi_status", ], ), pytest.param( "12:34:56:07:bb:3e", None, + None, marks=pytest.mark.xfail(reason="Invalid module names are not handled yet."), ), pytest.param( "", None, + None, marks=pytest.mark.xfail(reason="Invalid module names are not handled yet."), ), pytest.param( + None, None, None, marks=pytest.mark.xfail(reason="Invalid module names are not handled yet."), ), ], ) -def test_WeatherStationData_monitoredConditions(weatherStationData, module, expected): - assert sorted(weatherStationData.monitoredConditions(module)) == expected +def test_WeatherStationData_monitoredConditions( + weatherStationData, module, moduleId, expected +): + assert ( + sorted(weatherStationData.monitoredConditions(module=module, moduleId=moduleId)) + == expected + ) @freeze_time("2019-06-11") From 25083264450224606474a887d4fec33e62b0aade Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 20 Nov 2019 21:16:39 +0100 Subject: [PATCH 173/333] Add smoke detector support (#92) --- fixtures/camera_home_data.json | 44 ++++++++++++++++++- src/pyatmo/__main__.py | 6 ++- src/pyatmo/auth.py | 1 + src/pyatmo/camera.py | 79 +++++++++++++++++++++++++++++----- tests/test_pyatmo_camera.py | 50 +++++++++++++++++++++ 5 files changed, 167 insertions(+), 13 deletions(-) diff --git a/fixtures/camera_home_data.json b/fixtures/camera_home_data.json index 075fb8cf7..962c3b85b 100644 --- a/fixtures/camera_home_data.json +++ b/fixtures/camera_home_data.json @@ -84,7 +84,20 @@ "light_mode_status": "auto" } ], - "smokedetectors": [], + "smokedetectors": [ + { + "id": "12:34:56:00:8b:a2", + "type": "NSD", + "last_setup": 1567261859, + "name": "Hall" + }, + { + "id": "12:34:56:00:8b:ac", + "type": "NSD", + "last_setup": 1567262759, + "name": "Kitchen" + } + ], "events": [ { "id": "a1b2c3d4e5f6abcdef123456", @@ -176,6 +189,24 @@ "video_id": "12345678-1234-46cb-ad8f-23d89387409a", "video_status": "available", "message": "Bewegung erkannt" + }, + { + "id": "a1b2c3d4e5f6abcdef12345c", + "type": "sound_test", + "time": 1560506210, + "camera_id": "12:34:56:00:8b:a2", + "device_id": "12:34:56:00:8b:a2", + "sub_type": 0, + "message": "Hall: Alarmton erfolgreich getestet" + }, + { + "id": "a1b2c3d4e5f6abcdef12345d", + "type": "wifi_status", + "time": 1560506220, + "camera_id": "12:34:56:00:8b:a2", + "device_id": "12:34:56:00:8b:a2", + "sub_type": 1, + "message": "Hall:WLAN-Verbindung erfolgreich hergestellt" } ] }, @@ -202,6 +233,17 @@ } ], "smokedetectors": [] + }, + { + "id": "91763b24c43d3e344f424e8d", + "persons": [], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin" + }, + "cameras": [], + "smokedetectors": [] } ], "user": { diff --git a/src/pyatmo/__main__.py b/src/pyatmo/__main__.py index 5ce3605f9..8fa280130 100644 --- a/src/pyatmo/__main__.py +++ b/src/pyatmo/__main__.py @@ -33,7 +33,11 @@ def main(): clientSecret=CLIENT_SECRET, username=USERNAME, password=PASSWORD, - scope="read_station read_camera access_camera write_camera read_thermostat write_thermostat read_presence access_presence read_homecoach", + scope=( + "read_station read_camera access_camera read_thermostat " + "write_thermostat read_presence access_presence read_homecoach " + "read_smokedetector" + ), ) try: diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 599cde866..b18bd8761 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -27,6 +27,7 @@ class ClientAuth: read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) + read_smokedetector: to read the smoke detector status (Gethomedata) Several value can be used at the same time, ie: 'read_station read_camera' """ diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index b86c224c1..d161029b5 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -15,7 +15,7 @@ class CameraData: """ List the Netatmo camera informations - (Homes, cameras, modules, events, persons) + (Homes, cameras, smoke detectors, modules, events, persons) Args: authData (ClientAuth): Authentication information with a working access Token @@ -26,17 +26,18 @@ def __init__(self, authData, size=15): postParams = {"access_token": self.getAuthToken, "size": size} resp = postRequest(_GETHOMEDATA_REQ, postParams) if resp is None: - raise URLError("No camera data returned by Netatmo server") + raise URLError("No device data returned by Netatmo server") self.rawData = resp["body"].get("homes") if not self.rawData: - raise NoDevice("No camera data available") + raise NoDevice("No device data available") self.homes = {d["id"]: d for d in self.rawData} if not self.homes: - raise NoDevice("No camera available") + raise NoDevice("No device available") self.persons = {} self.events = {} self.outdoor_events = {} self.cameras = {} + self.smokedetectors = {} self.modules = {} self.lastEvent = {} self.outdoor_lastEvent = {} @@ -44,6 +45,7 @@ def __init__(self, authData, size=15): self.default_home = None self.default_home_id = None self.default_camera = None + self.default_smokedetector = None for item in self.rawData: homeId = item.get("id") nameHome = item.get("name") @@ -55,13 +57,16 @@ def __init__(self, authData, size=15): continue if homeId not in self.cameras: self.cameras[homeId] = {} + if homeId not in self.smokedetectors: + self.smokedetectors[homeId] = {} if homeId not in self.types: self.types[homeId] = {} for p in item["persons"]: self.persons[p["id"]] = p if "events" in item: - self.default_home = item["name"] - self.default_home_id = item["id"] + if not self.default_home and not self.default_home_id: + self.default_home = item["name"] + self.default_home_id = item["id"] for e in item["events"]: if e["type"] == "outdoor": if e["camera_id"] not in self.outdoor_events: @@ -77,8 +82,12 @@ def __init__(self, authData, size=15): for m in c["modules"]: self.modules[m["id"]] = m self.modules[m["id"]]["cam_id"] = c["id"] + for s in item["smokedetectors"]: + self.smokedetectors[homeId][s["id"]] = s for t in item["cameras"]: self.types[homeId][t["type"]] = t + for t in item["smokedetectors"]: + self.types[homeId][t["type"]] = t for camera in self.events: self.lastEvent[camera] = self.events[camera][ sorted(self.events[camera])[-1] @@ -176,6 +185,41 @@ def moduleByName(self, module=None, camera=None, home=None): return self.modules[key] return None + def smokedetectorById(self, sid): + for home, sd in self.smokedetectors.items(): + if sid in self.smokedetectors[home]: + return self.smokedetectors[home][sid] + return None + + def smokedetectorByName(self, smokedetector=None, home=None, home_id=None): + if home_id is None: + if home is None: + hid = self.default_home_id + else: + try: + hid = self.homeByName(home)["id"] + except InvalidHome: + LOG.debug("Invalid home %s", home) + return None + else: + hid = home_id + if smokedetector is None and home is None and home_id is None: + return self.default_smokedetector + elif not (home_id or home) and smokedetector: + for h_id, cam_ids in self.smokedetectors.items(): + for cam_id in cam_ids: + if self.smokedetectors[h_id][cam_id]["name"] == smokedetector: + return self.smokedetectors[h_id][cam_id] + elif hid and smokedetector: + hid = self.homeByName(home)["id"] + if hid not in self.smokedetectors: + return None + for cam_id in self.smokedetectors[hid]: + if self.smokedetectors[hid][cam_id]["name"] == smokedetector: + return self.smokedetectors[hid][cam_id] + else: + return list(self.smokedetectors[hid].values())[0] + def cameraType(self, camera=None, home=None, cid=None, home_id=None): """ Return the type of a given camera. @@ -293,29 +337,31 @@ def getProfileImage(self, name): return self.getCameraPicture(image_id, key) return None, None - def updateEvent(self, event=None, home=None, cameratype=None, home_id=None): + def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): """ Update the list of events """ if not home_id: try: + if not home: + home = self.default_home home_id = self.gethomeId(home) except InvalidHome: LOG.debug("Invalid Home %s", home) return None - if cameratype == "NACamera": + if devicetype == "NACamera": # for the Welcome camera if not event: - # If not event is provided we need to retrieve the oldest of + # If no event is provided we need to retrieve the oldest of # the last event seen by each camera listEvent = {} for cam_id in self.lastEvent: listEvent[self.lastEvent[cam_id]["time"]] = self.lastEvent[cam_id] event = listEvent[sorted(listEvent)[0]] - if cameratype == "NOC": + if devicetype == "NOC": # for the Presence camera if not event: - # If not event is provided we need to retrieve the oldest of + # If no event is provided we need to retrieve the oldest of # the last event seen by each camera listEvent = {} for cam_id in self.outdoor_lastEvent: @@ -323,6 +369,17 @@ def updateEvent(self, event=None, home=None, cameratype=None, home_id=None): self.outdoor_lastEvent[cam_id]["time"] ] = self.outdoor_lastEvent[cam_id] event = listEvent[sorted(listEvent)[0]] + if devicetype == "NSD": + # for the smoke detector + if not event: + # If no event is provided we need to retrieve the oldest of + # the last event by each smoke detector + listEvent = {} + for sid in self.outdoor_lastEvent: + listEvent[ + self.outdoor_lastEvent[sid]["time"] + ] = self.outdoor_lastEvent[sid] + event = listEvent[sorted(listEvent)[0]] postParams = { "access_token": self.getAuthToken, diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 66025e4b3..492c6850e 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -334,3 +334,53 @@ def test_CameraData_gethomeId(cameraHomeData): assert cameraHomeData.gethomeId("MYHOME") == "91763b24c43d3e344f424e8b" with pytest.raises(pyatmo.InvalidHome): assert cameraHomeData.gethomeId("InvalidName") + + +@pytest.mark.parametrize( + "sid, expected", + [ + ("12:34:56:00:8b:a2", "Hall"), + ("12:34:56:00:8b:ac", "Kitchen"), + ("None", None), + (None, None), + ], +) +def test_CameraData_smokedetectorById(cameraHomeData, sid, expected): + smokedetector = cameraHomeData.smokedetectorById(sid) + if smokedetector: + assert smokedetector["name"] == expected + else: + assert smokedetector is expected + + +@pytest.mark.parametrize( + "name, home, home_id, expected", + [ + ("Hall", None, None, "12:34:56:00:8b:a2"), + (None, None, None, None), + ("", None, None, "12:34:56:00:8b:a2"), + ("Hall", "MYHOME", None, "12:34:56:00:8b:a2"), + ("Hall", None, "91763b24c43d3e344f424e8b", "12:34:56:00:8b:a2"), + (None, None, "91763b24c43d3e344f424e8b", "12:34:56:00:8b:a2"), + (None, "MYHOME", None, "12:34:56:00:8b:a2"), + ("", "MYHOME", None, "12:34:56:00:8b:a2"), + ("Kitchen", "MYHOME", None, "12:34:56:00:8b:ac"), + (INVALID_NAME, None, None, None), + (None, INVALID_NAME, None, None), + ], +) +def test_CameraData_smokedetectorByName(cameraHomeData, name, home, home_id, expected): + if ( + home == INVALID_NAME + or name == INVALID_NAME + or (name is None and home is None and home_id is None) + ): + assert cameraHomeData.smokedetectorByName(name, home, home_id) is None + elif home_id is None: + assert cameraHomeData.smokedetectorByName(name, home)["id"] == expected + elif home is None: + assert ( + cameraHomeData.smokedetectorByName(name, home_id=home_id)["id"] == expected + ) + else: + assert cameraHomeData.smokedetectorByName(name, home, home_id)["id"] == expected From 6ac45ed29b3b69aab787ed88a1ea52e13c1ee771 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Wed, 20 Nov 2019 21:18:37 +0100 Subject: [PATCH 174/333] v3.1.0 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 62601edef..f890779c3 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="pyatmo", - version="3.0.1", # Should be updated with new versions + version="3.1.0", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", packages=find_packages(exclude=["tests"], where="src"), From 7c91d7cb859d5d2dace691760ea561fabbfa92d0 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sun, 1 Dec 2019 20:21:35 +0100 Subject: [PATCH 175/333] Catch errors when thermostat looses network connection (#95) * Catch errors when network error * Add test data --- fixtures/home_status_error_disconnected.json | 15 +++++++++++++++ src/pyatmo/thermostat.py | 7 ++++++- tests/test_pyatmo_thermostat.py | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 fixtures/home_status_error_disconnected.json diff --git a/fixtures/home_status_error_disconnected.json b/fixtures/home_status_error_disconnected.json new file mode 100644 index 000000000..0059e9b2a --- /dev/null +++ b/fixtures/home_status_error_disconnected.json @@ -0,0 +1,15 @@ +{ + "status":"ok", + "body":{ + "errors":[ + { + "code":6, + "id":"12:34:56:00:fa:d0" + } + ], + "home":{ + "id":"12:34:56:00:f1:62" + } + }, + "time_server":1559292039 +} \ No newline at end of file diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 5f2ac9b05..0491aca01 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -165,7 +165,12 @@ def __init__(self, authData, home_id=None, home=None): postParams = {"access_token": self.getAuthToken, "home_id": self.home_id} resp = postRequest(_GETHOMESTATUS_REQ, postParams) - if "errors" in resp or "body" not in resp or "home" not in resp["body"]: + if ( + "errors" in resp + or "body" not in resp + or "home" not in resp["body"] + or ("errors" in resp["body"] and "modules" not in resp["body"]["home"]) + ): raise NoDevice("No device found, errors in response") self.rawData = resp["body"]["home"] self.rooms = {} diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index de930a29a..4f8509831 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -463,3 +463,22 @@ def test_HomeData_setroomThermpoint_error( )["error"]["message"] == expected ) + + +def test_HomeStatus_error_disconnected(auth, requests_mock): + with open("fixtures/home_status_error_disconnected.json") as f: + json_fixture = json.load(f) + requests_mock.post( + pyatmo.thermostat._GETHOMESTATUS_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with open("fixtures/home_data_simple.json") as f: + json_fixture = json.load(f) + requests_mock.post( + pyatmo.thermostat._GETHOMESDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with pytest.raises(pyatmo.NoDevice): + pyatmo.HomeStatus(auth) From fd607d9c11fde1c3aa95b94dc41e220c8c2a9e20 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 28 Dec 2019 14:47:12 +0100 Subject: [PATCH 176/333] Refactor authentication (#96) * Implement oauth2 api for HA * Add HA account linking oauth api * more oauth * Remove artificial access token in params * Refactor * Refresh work * Fix refresh * Cleanup * Rename function * Fix test * Try to fix token refresh * Clean up code and fix tests * Update doc string * Fix typo * Add wrappers for non id based methods * Fix string * Fix variable name * Add pep8 conform methods which use only ids if possible * Add more id-only wrappers * Improve error handling --- Pipfile | 2 + setup.py | 2 +- src/pyatmo/__init__.py | 6 +- src/pyatmo/auth.py | 239 ++++++++++++++++++++++------- src/pyatmo/camera.py | 261 +++++++++++++++++++++++--------- src/pyatmo/exceptions.py | 4 + src/pyatmo/helpers.py | 24 +-- src/pyatmo/public_data.py | 13 +- src/pyatmo/thermostat.py | 57 ++++--- src/pyatmo/weather_station.py | 14 +- tests/test_pyatmo.py | 32 ++-- tests/test_pyatmo_camera.py | 6 +- tests/test_pyatmo_publicdata.py | 2 +- 13 files changed, 475 insertions(+), 187 deletions(-) diff --git a/Pipfile b/Pipfile index 878d5d865..727ec4553 100644 --- a/Pipfile +++ b/Pipfile @@ -21,6 +21,8 @@ pre-commit = "*" [packages] pyatmo = {editable = true,path = "."} +requests = "*" +requests-oauthlib = "*" [requires] python_version = "3.7" diff --git a/setup.py b/setup.py index f890779c3..21a85de52 100755 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ "Design for Home-Assitant (but not only)" ), long_description=open("README.md").read(), - install_requires=["requests"], + install_requires=["requests", "requests_oauthlib", "oauthlib"], ) diff --git a/src/pyatmo/__init__.py b/src/pyatmo/__init__.py index 0a54dc155..fe17aa3b3 100644 --- a/src/pyatmo/__init__.py +++ b/src/pyatmo/__init__.py @@ -1,6 +1,6 @@ -from .auth import ClientAuth +from .auth import ClientAuth, NetatmOAuth2 from .camera import CameraData -from .exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule +from .exceptions import ApiError, InvalidHome, InvalidRoom, NoDevice, NoSchedule from .home_coach import HomeCoachData from .public_data import PublicData from .thermostat import HomeData, HomeStatus @@ -14,6 +14,8 @@ "HomeStatus", "InvalidHome", "InvalidRoom", + "ApiError", + "NetatmOAuth2", "NoDevice", "NoSchedule", "PublicData", diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index b18bd8761..ad1ae2654 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -1,15 +1,187 @@ -import time +import logging +from typing import Callable, Dict, Optional, Tuple, Union -from .exceptions import NoDevice -from .helpers import _BASE_URL, LOG, postRequest +import requests +from oauthlib.oauth2 import LegacyApplicationClient, TokenExpiredError +from requests_oauthlib import OAuth2Session + +from .exceptions import ApiError +from .helpers import _BASE_URL, ERRORS + +LOG = logging.getLogger(__name__) # Common definitions _AUTH_REQ = _BASE_URL + "oauth2/token" +_AUTH_URL = _BASE_URL + "oauth2/authorize" _WEBHOOK_URL_ADD = _BASE_URL + "api/addwebhook" _WEBHOOK_URL_DROP = _BASE_URL + "api/dropwebhook" -class ClientAuth: +# Possible scops +ALL_SCOPES = [ + "read_station", + "read_camera", + "access_camera", + "write_camera", + "read_presence", + "access_presence", + "read_homecoach", + "read_smokedetector", + "read_thermostat", + "write_thermostat", +] + + +class NetatmOAuth2: + """ + Handle authentication with OAuth2 + + :param client_id: Application client ID delivered by Netatmo on dev.netatmo.com + :param client_secret: Application client secret delivered by Netatmo on dev.netatmo.com + :param redirect_uri: Redirect URI where to the authorization server will redirect with an authorization code + :param token: Authorization token + :param token_updater: Callback when the token is updated + :param scope: + read_station: to retrieve weather station data (Getstationsdata, Getmeasure) + read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture) + access_camera: to access the camera, the videos and the live stream + write_camera: to set home/away status of persons (Setpersonsaway, Setpersonshome) + read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata) + write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint) + read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) + access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status + read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) + read_smokedetector: to retrieve the smoke detector status (Gethomedata) + Several value can be used at the same time, ie: 'read_station read_camera' + """ + + def __init__( + self, + client_id: str = None, + client_secret: str = None, + redirect_uri: Optional[str] = None, + token: Optional[Dict[str, str]] = None, + token_updater: Optional[Callable[[str], None]] = None, + scope: Optional[str] = "read_station", + ): + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + self.token_updater = token_updater + self.scope = " ".join(ALL_SCOPES) if not scope else scope + + self.extra = { + "client_id": self.client_id, + "client_secret": self.client_secret, + } + + self._oauth = OAuth2Session( + client_id=self.client_id, + token=token, + token_updater=self.token_updater, + redirect_uri=self.redirect_uri, + scope=self.scope, + ) + + def refresh_tokens(self) -> Dict[str, Union[str, int]]: + """Refresh and return new tokens.""" + token = self._oauth.refresh_token(_AUTH_REQ) + + if self.token_updater is not None: + self.token_updater(token) + + return token + + def post_request( + self, url: str, params: Optional[Dict[str, str]] = None, timeout: int = 30 + ): + """Wrapper for post requests.""" + if not params: + params = {} + + if "http://" in url: + try: + resp = requests.post(url, data=params, timeout=timeout) + except requests.exceptions.ChunkedEncodingError: + LOG.debug("Encoding error when connecting to '%s'", url) + else: + + def query(url, params, timeout, retries): + if retries == 0: + LOG.error("Too many retries") + return + try: + return self._oauth.post(url=url, data=params, timeout=timeout) + except TokenExpiredError: + self._oauth.token = self.refresh_tokens() + return query(url, params, timeout, retries - 1) + + resp = query(url, params, timeout, 3) + + if not resp: + raise ApiError(f"Error when accessing '{url}'") + + if not resp.ok: + LOG.debug("The Netatmo API returned %s", resp.status_code) + LOG.debug("Netato API error: %s", resp.content) + if resp.status_code == 404: + raise ApiError( + f"{resp.status_code} - " + f"{ERRORS[resp.status_code]} - " + f"when accessing '{url}'" + ) + else: + raise ApiError( + f"{resp.status_code} - " + f"{ERRORS[resp.status_code]} - " + f"{resp.json()['error']['message']} " + f"({resp.json()['error']['code']}) " + f"when accessing '{url}'" + ) + + try: + return ( + resp.json() + if "application/json" in resp.headers.get("content-type") + else resp.content + ) + except TypeError: + LOG.debug("Invalid response %s", resp) + return None + + def get_authorization_url(self, state: Optional[str] = None) -> Tuple[str, str]: + return self._oauth.authorization_url(_AUTH_URL, state) + + def request_token( + self, authorization_response: Optional[str] = None, code: Optional[str] = None + ) -> Dict[str, str]: + """ + Generic method for fetching a Netatmo access token. + :param authorization_response: Authorization response URL, the callback + URL of the request back to you. + :param code: Authorization code + :return: A token dict + """ + return self._oauth.fetch_token( + _AUTH_REQ, + authorization_response=authorization_response, + code=code, + client_secret=self.client_secret, + include_client_id=True, + ) + + def addwebhook(self, webhook_url): + postParams = {"url": webhook_url} + resp = self.post_request(_WEBHOOK_URL_ADD, postParams) + LOG.debug("addwebhook: %s", resp) + + def dropwebhook(self): + postParams = {"app_types": "app_security"} + resp = self.post_request(_WEBHOOK_URL_DROP, postParams) + LOG.debug("dropwebhook: %s", resp) + + +class ClientAuth(NetatmOAuth2): """ Request authentication and keep access token available through token method. Renew it automatically if necessary Args: @@ -20,66 +192,29 @@ class ClientAuth: scope (Optional[str]): read_station: to retrieve weather station data (Getstationsdata, Getmeasure) read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture) - access_camera: to access the camera, the videos and the live stream. + access_camera: to access the camera, the videos and the live stream write_camera: to set home/away status of persons (Setpersonsaway, Setpersonshome) read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata) write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint) read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) - read_smokedetector: to read the smoke detector status (Gethomedata) + read_smokedetector: to retrieve the smoke detector status (Gethomedata) Several value can be used at the same time, ie: 'read_station read_camera' """ def __init__( self, clientId, clientSecret, username, password, scope="read_station" ): - postParams = { - "grant_type": "password", - "client_id": clientId, - "client_secret": clientSecret, - "username": username, - "password": password, - "scope": scope, - } - resp = postRequest(_AUTH_REQ, postParams) self._clientId = clientId self._clientSecret = clientSecret - try: - self._accessToken = resp["access_token"] - except KeyError: - LOG.error("Netatmo API returned %s", resp["error"]) - raise NoDevice("Authentication against Netatmo API failed") - self.refreshToken = resp["refresh_token"] - self._scope = resp["scope"] - self.expiration = int(resp["expire_in"] + time.time() - 1800) - - def addwebhook(self, webhook_url): - postParams = { - "access_token": self._accessToken, - "url": webhook_url, - "app_types": "app_security", - } - resp = postRequest(_WEBHOOK_URL_ADD, postParams) - LOG.debug("addwebhook: %s", resp) - - def dropwebhook(self): - postParams = {"access_token": self._accessToken, "app_types": "app_security"} - resp = postRequest(_WEBHOOK_URL_DROP, postParams) - LOG.debug("dropwebhook: %s", resp) - @property - def accessToken(self): - - if self.expiration < time.time(): # Token should be renewed - postParams = { - "grant_type": "refresh_token", - "refresh_token": self.refreshToken, - "client_id": self._clientId, - "client_secret": self._clientSecret, - } - resp = postRequest(_AUTH_REQ, postParams) - self._accessToken = resp["access_token"] - self.refreshToken = resp["refresh_token"] - self.expiration = int(resp["expire_in"] + time.time() - 1800) - return self._accessToken + self._oauth = OAuth2Session(client=LegacyApplicationClient(client_id=clientId)) + self._oauth.fetch_token( + token_url=_AUTH_REQ, + username=username, + password=password, + client_id=clientId, + client_secret=clientSecret, + scope=scope, + ) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index d161029b5..f4d7dde1f 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -1,15 +1,16 @@ import imghdr import time -from urllib.error import URLError +from typing import Dict, Tuple -from .exceptions import InvalidHome, NoDevice -from .helpers import _BASE_URL, LOG, postRequest +from .exceptions import ApiError, InvalidHome, NoDevice +from .helpers import _BASE_URL, LOG _GETHOMEDATA_REQ = _BASE_URL + "api/gethomedata" _GETCAMERAPICTURE_REQ = _BASE_URL + "api/getcamerapicture" _GETEVENTSUNTIL_REQ = _BASE_URL + "api/geteventsuntil" _SETPERSONSAWAY_REQ = _BASE_URL + "api/setpersonsaway" _SETPERSONSHOME_REQ = _BASE_URL + "api/setpersonshome" +_SETSTATE_REQ = _BASE_URL + "api/setstate" class CameraData: @@ -22,11 +23,11 @@ class CameraData: """ def __init__(self, authData, size=15): - self.getAuthToken = authData.accessToken - postParams = {"access_token": self.getAuthToken, "size": size} - resp = postRequest(_GETHOMEDATA_REQ, postParams) - if resp is None: - raise URLError("No device data returned by Netatmo server") + self.authData = authData + postParams = {"size": size} + resp = self.authData.post_request(url=_GETHOMEDATA_REQ, params=postParams) + if resp is None or "body" not in resp: + raise NoDevice("No device data returned by Netatmo server") self.rawData = resp["body"].get("homes") if not self.rawData: raise NoDevice("No device data available") @@ -131,12 +132,18 @@ def gethomeId(self, home=None): raise InvalidHome("Invalid Home %s" % home) def cameraById(self, cid): - for home, _ in self.cameras.items(): - if cid in self.cameras[home]: - return self.cameras[home][cid] + """Get camera data by ID.""" + return self.get_camera(cid) + + def get_camera(self, cid: str) -> Dict[str, str]: + """Get camera data.""" + for home_id, _ in self.cameras.items(): + if cid in self.cameras[home_id]: + return self.cameras[home_id][cid] return None def cameraByName(self, camera=None, home=None, home_id=None): + """Get camera data by name.""" if home_id is None: if home is None: hid = self.default_home_id @@ -156,7 +163,6 @@ def cameraByName(self, camera=None, home=None, home_id=None): if self.cameras[h_id][cam_id]["name"] == camera: return self.cameras[h_id][cam_id] elif hid and camera: - hid = self.homeByName(home)["id"] if hid not in self.cameras: return None for cam_id in self.cameras[hid]: @@ -226,54 +232,75 @@ def cameraType(self, camera=None, home=None, cid=None, home_id=None): """ cameratype = None if cid: - camera_data = self.cameraById(cid) + camera_data = self.get_camera(cid) else: camera_data = self.cameraByName(camera=camera, home=home, home_id=home_id) if camera_data: cameratype = camera_data["type"] return cameratype - def cameraUrls(self, camera=None, home=None, cid=None, home_id=None): + def cameraUrls( + self, camera: str = None, home: str = None, cid: str = None, home_id: str = None + ) -> Tuple[str, str]: + """ + Return the vpn_url and the local_url (if available) of a given camera + in order to access its live feed + (old interface) + """ + if home_id: + cid = self.cameraByName(camera=camera, home_id=home_id)["id"] + else: + cid = self.cameraByName(camera=camera, home=home)["id"] + return self.camera_urls(cid=cid) + + def camera_urls(self, cid: str) -> Tuple[str, str]: """ Return the vpn_url and the local_url (if available) of a given camera in order to access its live feed """ local_url = None vpn_url = None - if cid: - camera_data = self.cameraById(cid) - elif home_id: - camera_data = self.cameraByName(camera=camera, home_id=home_id) - else: - camera_data = self.cameraByName(camera=camera, home=home) + + camera_data = self.get_camera(cid) if camera_data: vpn_url = camera_data.get("vpn_url") if camera_data.get("is_local"): - try: - resp = postRequest("{0}/command/ping".format(vpn_url), {}) - temp_local_url = resp["local_url"] - except URLError: - return None, None - try: - resp = postRequest("{0}/command/ping".format(temp_local_url), {}) - if temp_local_url == resp["local_url"]: - local_url = temp_local_url - except URLError: - pass + def check_url(url): + if url is None: + return None + try: + resp = self.authData.post_request(url=f"{url}/command/ping") + except ApiError: + return None + else: + return resp.get("local_url") + + temp_local_url = check_url(vpn_url) + local_url = check_url(temp_local_url) return vpn_url, local_url + def get_light_state(self, cid: str) -> str: + """Return the current mode of the floodlight of a presence camera.""" + return self.get_camera(cid).get("light_mode_status") + def personsAtHome(self, home=None, home_id=None): """ Return the list of known persons who are currently at home + (old interface) """ - if home_id: - home_data = self.homeById(home_id) + if not home: + home_id = self.default_home_id else: - if not home: - home = self.default_home - home_data = self.homeByName(home) + home_id = self.homeByName(home)["id"] + return self.persons_at_home(home_id) + + def persons_at_home(self, home_id=None): + """ + Return the list of known persons who are currently at home + """ + home_data = self.homeById(home_id) atHome = [] for p in home_data["persons"]: # Only check known personshome @@ -283,30 +310,63 @@ def personsAtHome(self, home=None, home_id=None): return atHome def setPersonsHome(self, person_ids, home_id): + """ + Mark persons as home. + (old interface) + """ + return self.set_persons_home(person_ids, home_id) + + def set_persons_home(self, person_ids, home_id): """ Mark persons as home. """ postParams = { - "access_token": self.getAuthToken, "home_id": home_id, "person_ids[]": person_ids, } - resp = postRequest(_SETPERSONSHOME_REQ, postParams) + resp = self.authData.post_request(url=_SETPERSONSHOME_REQ, params=postParams) return resp def setPersonsAway(self, person_id, home_id): """ Mark a person as away or set the whole home to being empty. + (old interface) + """ + return self.set_persons_away(person_id, home_id) + + def set_persons_away(self, person_id, home_id): + """Mark a person as away or set the whole home to being empty. + + Arguments: + person_id {str} -- ID of a person + home_id {str} -- ID of a home + + Returns: + [type] -- [description] """ postParams = { - "access_token": self.getAuthToken, "home_id": home_id, "person_id": person_id, } - resp = postRequest(_SETPERSONSAWAY_REQ, postParams) + resp = self.authData.post_request(url=_SETPERSONSAWAY_REQ, params=postParams) return resp def getPersonId(self, name): + """ + Retrieve a persons id + (old interface) + """ + return self.get_person_id(name) + + def get_person_id(self, name): + """Retrieve the ID of a person + + Arguments: + name {str} -- Name of a person + + Returns: + str -- ID of a person + """ for pid, data in self.persons.items(): if "pseudo" in data and name == data["pseudo"]: return pid @@ -317,11 +377,10 @@ def getCameraPicture(self, image_id, key): Download a specific image (of an event or user face) from the camera """ postParams = { - "access_token": self.getAuthToken, "image_id": image_id, "key": key, } - resp = postRequest(_GETCAMERAPICTURE_REQ, postParams) + resp = self.authData.post_request(url=_GETCAMERAPICTURE_REQ, params=postParams) image_type = imghdr.what("NONE.FILE", resp) return resp, image_type @@ -349,6 +408,7 @@ def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): except InvalidHome: LOG.debug("Invalid Home %s", home) return None + if devicetype == "NACamera": # for the Welcome camera if not event: @@ -358,6 +418,7 @@ def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): for cam_id in self.lastEvent: listEvent[self.lastEvent[cam_id]["time"]] = self.lastEvent[cam_id] event = listEvent[sorted(listEvent)[0]] + if devicetype == "NOC": # for the Presence camera if not event: @@ -369,6 +430,7 @@ def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): self.outdoor_lastEvent[cam_id]["time"] ] = self.outdoor_lastEvent[cam_id] event = listEvent[sorted(listEvent)[0]] + if devicetype == "NSD": # for the smoke detector if not event: @@ -382,12 +444,21 @@ def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): event = listEvent[sorted(listEvent)[0]] postParams = { - "access_token": self.getAuthToken, "home_id": home_id, "event_id": event["id"], } - resp = postRequest(_GETEVENTSUNTIL_REQ, postParams) - eventList = resp["body"]["events_list"] + try: + resp = self.authData.post_request( + url=_GETEVENTSUNTIL_REQ, params=postParams + ) + eventList = resp["body"]["events_list"] + except ApiError: + pass + except KeyError: + LOG.debug("eventList response: %s", resp) + LOG.debug("eventList body: %s", resp["body"]) + eventList = [] + for e in eventList: if e["type"] == "outdoor": if e["camera_id"] not in self.outdoor_events: @@ -409,26 +480,33 @@ def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): def personSeenByCamera(self, name, home=None, camera=None, exclude=0): """ Evaluate if a specific person has been seen + (old interface) """ try: - cam_id = self.cameraByName(camera=camera, home=home)["id"] + cid = self.cameraByName(camera=camera, home=home)["id"] except TypeError: LOG.error("personSeenByCamera: Camera name or home is unknown") return False + return self.person_seen_by_camera(name=name, cid=cid, exclude=exclude) + + def person_seen_by_camera(self, name, cid, exclude=0): + """ + Evaluate if a specific person has been seen + """ # Check in the last event is someone known has been seen if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cam_id], reverse=True) + array_time_event = sorted(self.events[cid], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False - if self.events[cam_id][time_ev]["type"] == "person": - person_id = self.events[cam_id][time_ev]["person_id"] + if self.events[cid][time_ev]["type"] == "person": + person_id = self.events[cid][time_ev]["person_id"] if "pseudo" in self.persons[person_id]: if self.persons[person_id]["pseudo"] == name: return True - elif self.lastEvent[cam_id]["type"] == "person": - person_id = self.lastEvent[cam_id]["person_id"] + elif self.lastEvent[cid]["type"] == "person": + person_id = self.lastEvent[cid]["person_id"] if "pseudo" in self.persons[person_id]: if self.persons[person_id]["pseudo"] == name: return True @@ -453,6 +531,7 @@ def knownPersonsNames(self): def someoneKnownSeen(self, home=None, camera=None, exclude=0, cid=None): """ Evaluate if someone known has been seen + (old interface) """ if not cid: try: @@ -460,7 +539,12 @@ def someoneKnownSeen(self, home=None, camera=None, exclude=0, cid=None): except TypeError: LOG.error("someoneKnownSeen: Camera name or home is unknown") return False + return self.someone_known_seen(cid=cid, exclude=exclude) + def someone_known_seen(self, cid, exclude=0): + """ + Evaluate if someone known has been seen + """ if exclude: limit = time.time() - exclude array_time_event = sorted(self.events[cid], reverse=True) @@ -479,6 +563,7 @@ def someoneKnownSeen(self, home=None, camera=None, exclude=0, cid=None): def someoneUnknownSeen(self, home=None, camera=None, exclude=0, cid=None): """ Evaluate if someone unknown has been seen + (old interface) """ if not cid: try: @@ -486,7 +571,9 @@ def someoneUnknownSeen(self, home=None, camera=None, exclude=0, cid=None): except TypeError: LOG.error("someoneUnknownSeen: Camera name or home is unknown") return False + return self.someone_unknown_seen(cid=cid, exclude=exclude) + def someone_unknown_seen(self, cid, exclude=0): if exclude: limit = time.time() - exclude array_time_event = sorted(self.events[cid], reverse=True) @@ -508,6 +595,7 @@ def someoneUnknownSeen(self, home=None, camera=None, exclude=0, cid=None): def motionDetected(self, home=None, camera=None, exclude=0, cid=None): """ Evaluate if movement has been detected + (old interface) """ if not cid: try: @@ -515,7 +603,12 @@ def motionDetected(self, home=None, camera=None, exclude=0, cid=None): except TypeError: LOG.error("motionDetected: Camera name or home is unknown") return False + return self.motion_detected(cid=cid, exclude=exclude) + def motion_detected(self, cid, exclude=0): + """ + Evaluate if movement has been detected + """ if exclude: limit = time.time() - exclude array_time_event = sorted(self.events[cid], reverse=True) @@ -531,6 +624,7 @@ def motionDetected(self, home=None, camera=None, exclude=0, cid=None): def outdoormotionDetected(self, home=None, camera=None, offset=0, cid=None): """ Evaluate if outdoor movement has been detected + (old interface) """ if not cid: try: @@ -538,7 +632,12 @@ def outdoormotionDetected(self, home=None, camera=None, offset=0, cid=None): except TypeError: LOG.error("outdoormotionDetected: Camera name or home is unknown") return False + return self.outdoor_motion_detected(cid=cid, offset=0) + def outdoor_motion_detected(self, cid, offset=0): + """ + Evaluate if outdoor movement has been detected + """ if cid in self.lastEvent: if self.lastEvent[cid]["type"] == "movement": if self.lastEvent[cid][ @@ -552,6 +651,7 @@ def outdoormotionDetected(self, home=None, camera=None, offset=0, cid=None): def humanDetected(self, home=None, camera=None, offset=0, cid=None): """ Evaluate if a human has been detected + (old interface) """ if not cid: try: @@ -559,7 +659,12 @@ def humanDetected(self, home=None, camera=None, offset=0, cid=None): except TypeError: LOG.error("personSeenByCamera: Camera name or home is unknown") return False + return self.human_detected(cid=cid, offset=0) + def human_detected(self, cid, offset=0): + """ + Evaluate if a human has been detected + """ if self.outdoor_lastEvent[cid]["video_status"] == "recording": for e in self.outdoor_lastEvent[cid]["event_list"]: if e["type"] == "human" and e["time"] + offset > int(time.time()): @@ -569,6 +674,7 @@ def humanDetected(self, home=None, camera=None, offset=0, cid=None): def animalDetected(self, home=None, camera=None, offset=0, cid=None): """ Evaluate if an animal has been detected + (old interface) """ if not cid: try: @@ -576,7 +682,12 @@ def animalDetected(self, home=None, camera=None, offset=0, cid=None): except TypeError: LOG.error("animalDetected: Camera name or home is unknown") return False + return self.animal_detected(cid=cid, offset=0) + def animal_detected(self, cid, offset=0): + """ + Evaluate if an animal has been detected + """ if self.outdoor_lastEvent[cid]["video_status"] == "recording": for e in self.outdoor_lastEvent[cid]["event_list"]: if e["type"] == "animal" and e["time"] + offset > int(time.time()): @@ -586,6 +697,7 @@ def animalDetected(self, home=None, camera=None, offset=0, cid=None): def carDetected(self, home=None, camera=None, offset=0, cid=None): """ Evaluate if a car has been detected + (old interface) """ if not cid: try: @@ -593,7 +705,12 @@ def carDetected(self, home=None, camera=None, offset=0, cid=None): except TypeError: LOG.error("carDetected: Camera name or home is unknown") return False + return self.car_detected(cid=cid, offset=offset) + def car_detected(self, cid, offset=0): + """ + Evaluate if a car has been detected + """ if self.outdoor_lastEvent[cid]["video_status"] == "recording": for e in self.outdoor_lastEvent[cid]["event_list"]: if e["type"] == "vehicle" and e["time"] + offset > int(time.time()): @@ -603,32 +720,38 @@ def carDetected(self, home=None, camera=None, offset=0, cid=None): def moduleMotionDetected(self, module=None, home=None, camera=None, exclude=0): """ Evaluate if movement has been detected + (old interface) """ try: mod = self.moduleByName(module, camera=camera, home=home) - mod_id = mod["id"] - cam_id = mod["cam_id"] + mid = mod["id"] + cid = mod["cam_id"] except TypeError: LOG.error( - "moduleMotionDetected: Module name or" "Camera name or home is unknown" + "moduleMotionDetected: Module name or Camera name or home is unknown" ) return False + return self.module_motion_detected(mid=mid, cid=cid, exclude=exclude) + def module_motion_detected(self, mid, cid, exclude=0): + """ + Evaluate if movement has been detected + """ if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cam_id], reverse=True) + array_time_event = sorted(self.events[cid], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False if ( - self.events[cam_id][time_ev]["type"] == "tag_big_move" - or self.events[cam_id][time_ev]["type"] == "tag_small_move" - ) and self.events[cam_id][time_ev]["module_id"] == mod_id: + self.events[cid][time_ev]["type"] == "tag_big_move" + or self.events[cid][time_ev]["type"] == "tag_small_move" + ) and self.events[cid][time_ev]["module_id"] == mid: return True elif ( - self.lastEvent[cam_id]["type"] == "tag_big_move" - or self.lastEvent[cam_id]["type"] == "tag_small_move" - ) and self.lastEvent[cam_id]["module_id"] == mod_id: + self.lastEvent[cid]["type"] == "tag_big_move" + or self.lastEvent[cid]["type"] == "tag_small_move" + ) and self.lastEvent[cid]["module_id"] == mid: return True return False @@ -638,26 +761,28 @@ def moduleOpened(self, module=None, home=None, camera=None, exclude=0): """ try: mod = self.moduleByName(module, camera=camera, home=home) - mod_id = mod["id"] - cam_id = mod["cam_id"] + mid = mod["id"] + cid = mod["cam_id"] except TypeError: LOG.error("moduleOpened: Camera name, or home, or module is unknown") return False + return self.module_opened(mid=mid, cid=cid, exclude=exclude) + def module_opened(self, mid, cid, exclude=0): if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cam_id], reverse=True) + array_time_event = sorted(self.events[cid], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False if ( - self.events[cam_id][time_ev]["type"] == "tag_open" - and self.events[cam_id][time_ev]["module_id"] == mod_id + self.events[cid][time_ev]["type"] == "tag_open" + and self.events[cid][time_ev]["module_id"] == mid ): return True elif ( - self.lastEvent[cam_id]["type"] == "tag_open" - and self.lastEvent[cam_id]["module_id"] == mod_id + self.lastEvent[cid]["type"] == "tag_open" + and self.lastEvent[cid]["module_id"] == mid ): return True return False diff --git a/src/pyatmo/exceptions.py b/src/pyatmo/exceptions.py index 651eb9829..1e694e98b 100644 --- a/src/pyatmo/exceptions.py +++ b/src/pyatmo/exceptions.py @@ -12,3 +12,7 @@ class InvalidRoom(Exception): class NoDevice(Exception): pass + + +class ApiError(Exception): + pass diff --git a/src/pyatmo/helpers.py b/src/pyatmo/helpers.py index 87c4f066f..43fa25ab1 100644 --- a/src/pyatmo/helpers.py +++ b/src/pyatmo/helpers.py @@ -3,26 +3,18 @@ from calendar import timegm from datetime import datetime -import requests - LOG = logging.getLogger(__name__) _BASE_URL = "https://api.netatmo.com/" - -def postRequest(url, params=None, timeout=30): - resp = requests.post(url, data=params, timeout=timeout) - if not resp.ok: - LOG.error("The Netatmo API returned %s", resp.status_code) - try: - return ( - resp.json() - if "application/json" in resp.headers.get("content-type") - else resp.content - ) - except TypeError: - LOG.debug("Invalid response %s", resp) - return None +ERRORS = { + 400: "Bad request", + 401: "Unauthorized", + 403: "Forbidden", + 404: "Not found", + 406: "Not Acceptable", + 500: "Internal Server Error", +} def toTimeString(value): diff --git a/src/pyatmo/public_data.py b/src/pyatmo/public_data.py index e7e2025ea..18e655b82 100644 --- a/src/pyatmo/public_data.py +++ b/src/pyatmo/public_data.py @@ -1,5 +1,5 @@ from .exceptions import NoDevice -from .helpers import _BASE_URL, postRequest, toTimeString +from .helpers import _BASE_URL, toTimeString _GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata" _LON_NE = 6.221652 @@ -25,7 +25,7 @@ class PublicData: def __init__( self, - auth_data, + authData, LAT_NE=_LAT_NE, LON_NE=_LON_NE, LAT_SW=_LAT_SW, @@ -33,9 +33,8 @@ def __init__( required_data_type=None, # comma-separated list from above _STATION or _ACCESSORY values filtering=False, ): - self.getAuthToken = auth_data.accessToken - post_params = { - "access_token": self.getAuthToken, + self.authData = authData + postParams = { "lat_ne": LAT_NE, "lon_ne": LON_NE, "lat_sw": LAT_SW, @@ -44,9 +43,9 @@ def __init__( } if required_data_type: - post_params["required_data"] = required_data_type + postParams["required_data"] = required_data_type - resp = postRequest(_GETPUBLIC_DATA, post_params) + resp = self.authData.post_request(url=_GETPUBLIC_DATA, params=postParams) try: self.raw_data = resp["body"] except (KeyError, TypeError): diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 0491aca01..a59e92506 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -1,7 +1,7 @@ import logging from .exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule -from .helpers import _BASE_URL, postRequest +from .helpers import _BASE_URL LOG = logging.getLogger(__name__) @@ -22,10 +22,9 @@ class HomeData: """ def __init__(self, authData): - self.getAuthToken = authData.accessToken - postParams = {"access_token": self.getAuthToken} - resp = postRequest(_GETHOMESDATA_REQ, postParams) - if resp is None: + self.authData = authData + resp = self.authData.post_request(url=_GETHOMESDATA_REQ) + if resp is None or "body" not in resp: raise NoDevice("No thermostat data returned by Netatmo server") self.rawData = resp["body"].get("homes") if not self.rawData: @@ -114,20 +113,24 @@ def getSelectedschedule(self, home=None, home_id=None): if not home: home = self.default_home home_id = self.gethomeId(home=home) + return self.get_selected_schedule(home_id=home_id) + def get_selected_schedule(self, home_id: str): + """Get the selected schedule for a given home ID.""" try: - schedule = self.schedules[home_id] + schedules = self.schedules[home_id] except KeyError: raise NoSchedule("No schedules available for %s" % home_id) - for key in schedule.keys(): - if "selected" in schedule[key].keys(): - return schedule[key] + for key in schedules.keys(): + if "selected" in schedules[key].keys(): + return schedules[key] def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): if home is None: home = self.default_home home_id = self.gethomeId(home=home) + schedules = { self.schedules[home_id][s]["name"]: self.schedules[home_id][s]["id"] for s in self.schedules[home_id] @@ -141,36 +144,54 @@ def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): schedule_id = schedules[schedule] else: raise NoSchedule("No schedule specified") + + return self.switch_home_schedule(schedule_id=schedule_id, home_id=home_id) + + def switch_home_schedule(self, schedule_id: str, home_id: str) -> bool: + """.""" + try: + schedules = self.schedules[home_id] + except KeyError: + raise NoSchedule("No schedules available for %s" % home_id) + + schedules = { + self.schedules[home_id][s]["name"]: self.schedules[home_id][s]["id"] + for s in self.schedules[home_id] + } + if schedule_id not in list(schedules.values()): + raise NoSchedule("%s is not a valid schedule id" % schedule_id) + postParams = { - "access_token": self.getAuthToken, "home_id": home_id, "schedule_id": schedule_id, } - resp = postRequest(_SWITCHHOMESCHEDULE_REQ, postParams) + resp = self.authData.post_request( + url=_SWITCHHOMESCHEDULE_REQ, params=postParams + ) LOG.debug("Response: %s", resp) class HomeStatus: def __init__(self, authData, home_id=None, home=None): - self.getAuthToken = authData.accessToken + self.authData = authData self.home_data = HomeData(authData) if home_id is not None: self.home_id = home_id - LOG.debug("home_id: %s", self.home_id) elif home is not None: self.home_id = self.home_data.gethomeId(home=home) else: self.home_id = self.home_data.gethomeId(home=self.home_data.default_home) - postParams = {"access_token": self.getAuthToken, "home_id": self.home_id} + postParams = {"home_id": self.home_id} - resp = postRequest(_GETHOMESTATUS_REQ, postParams) + resp = self.authData.post_request(url=_GETHOMESTATUS_REQ, params=postParams) if ( "errors" in resp or "body" not in resp or "home" not in resp["body"] or ("errors" in resp["body"] and "modules" not in resp["body"]["home"]) ): + LOG.error("Errors in response: %s", resp) raise NoDevice("No device found, errors in response") self.rawData = resp["body"]["home"] self.rooms = {} @@ -322,19 +343,17 @@ def thermostatType(self, home, rid, home_id=None): def setThermmode(self, home_id, mode): postParams = { - "access_token": self.getAuthToken, "home_id": home_id, "mode": mode, } - return postRequest(_SETTHERMMODE_REQ, postParams) + return self.authData.post_request(url=_SETTHERMMODE_REQ, params=postParams) def setroomThermpoint(self, home_id, room_id, mode, temp=None): postParams = { - "access_token": self.getAuthToken, "home_id": home_id, "room_id": room_id, "mode": mode, } if temp is not None: postParams["temp"] = temp - return postRequest(_SETROOMTHERMPOINT_REQ, postParams) + return self.authData.post_request(url=_SETROOMTHERMPOINT_REQ, params=postParams) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 1039d22d6..79be749d5 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -2,7 +2,7 @@ import time from .exceptions import NoDevice -from .helpers import _BASE_URL, postRequest, todayStamps +from .helpers import _BASE_URL, todayStamps LOG = logging.getLogger(__name__) @@ -20,10 +20,9 @@ class WeatherStationData: def __init__(self, authData, urlReq=None): """Initialize the weather station class.""" self.urlReq = urlReq or _GETSTATIONDATA_REQ - self.getAuthToken = authData.accessToken - postParams = {"access_token": self.getAuthToken} - resp = postRequest(self.urlReq, postParams) - if resp is None: + self.authData = authData + resp = self.authData.post_request(url=self.urlReq) + if resp is None or "body" not in resp: raise NoDevice("No weather station data returned by Netatmo server") try: self.rawData = resp["body"].get("devices") @@ -252,8 +251,7 @@ def getMeasure( optimize=False, real_time=False, ): - postParams = {"access_token": self.getAuthToken} - postParams["device_id"] = device_id + postParams = {"device_id": device_id} if module_id: postParams["module_id"] = module_id postParams["scale"] = scale @@ -266,7 +264,7 @@ def getMeasure( postParams["limit"] = limit postParams["optimize"] = "true" if optimize else "false" postParams["real_time"] = "true" if real_time else "false" - return postRequest(_GETMEASURE_REQ, postParams) + return self.authData.post_request(url=_GETMEASURE_REQ, params=postParams) def MinMaxTH(self, station=None, module=None, frame="last24"): if not station: diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index 236cb52ce..892811e20 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -4,14 +4,18 @@ import pytest import requests +import oauthlib import pyatmo def test_ClientAuth(auth): - assert auth.accessToken == ( + assert auth._oauth.token["access_token"] == ( "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12" ) + assert auth._oauth.token["refresh_token"] == ( + "91763b24c43d3e344f424e8b|87ff8755a00c6b8a120b55a08c758e93" + ) def test_ClientAuth_invalid(requests_mock): @@ -22,7 +26,7 @@ def test_ClientAuth_invalid(requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - with pytest.raises(pyatmo.exceptions.NoDevice): + with pytest.raises(oauthlib.oauth2.rfc6749.errors.InvalidGrantError): pyatmo.ClientAuth( clientId="CLIENT_ID", clientSecret="CLIENT_SECRET", @@ -31,41 +35,45 @@ def test_ClientAuth_invalid(requests_mock): ) -def test_postRequest_json(requests_mock): +def test_postRequest_json(auth, requests_mock): """Test wrapper for posting requests against the Netatmo API.""" requests_mock.post( - pyatmo.helpers._BASE_URL, + pyatmo.auth._BASE_URL, json={"a": "b"}, headers={"content-type": "application/json"}, ) - resp = pyatmo.helpers.postRequest(pyatmo.helpers._BASE_URL, None) + resp = auth.post_request(pyatmo.auth._BASE_URL, None) assert resp == {"a": "b"} -def test_postRequest_binary(requests_mock): +def test_postRequest_binary(auth, requests_mock): """Test wrapper for posting requests against the Netatmo API.""" requests_mock.post( pyatmo.helpers._BASE_URL, text="Success", headers={"content-type": "application/text"}, ) - resp = pyatmo.helpers.postRequest(pyatmo.helpers._BASE_URL, None) + resp = auth.post_request(pyatmo.helpers._BASE_URL, None) assert resp == b"Success" @pytest.mark.parametrize("test_input,expected", [(200, None), (404, None)]) -def test_postRequest_fail(requests_mock, test_input, expected): +def test_postRequest_fail(auth, requests_mock, test_input, expected): """Test failing requests against the Netatmo API.""" requests_mock.post(pyatmo.helpers._BASE_URL, status_code=test_input) - resp = pyatmo.helpers.postRequest(pyatmo.helpers._BASE_URL, None) - assert resp is expected + if test_input == 200: + resp = auth.post_request(pyatmo.helpers._BASE_URL, None) + assert resp is expected + else: + with pytest.raises(pyatmo.ApiError): + resp = auth.post_request(pyatmo.helpers._BASE_URL, None) -def test_postRequest_timeout(requests_mock): +def test_postRequest_timeout(auth, requests_mock): """Test failing requests against the Netatmo API with timeouts.""" requests_mock.post(pyatmo.helpers._BASE_URL, exc=requests.exceptions.ConnectTimeout) with pytest.raises(requests.exceptions.ConnectTimeout): - assert pyatmo.helpers.postRequest(pyatmo.helpers._BASE_URL, None) + assert auth.post_request(pyatmo.helpers._BASE_URL, None) @pytest.mark.parametrize( diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 492c6850e..96772267e 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -76,12 +76,13 @@ def test_CameraData_cameraById(cameraHomeData, cid, expected): (None, "MYHOME", None, "12:34:56:00:f1:62"), ("", "MYHOME", None, "12:34:56:00:f1:62"), ("Garden", "MYHOME", None, "12:34:56:00:a5:a4"), + ("Garden", None, "InvalidHomeID", "12:34:56:00:a5:a4"), (INVALID_NAME, None, None, None), (None, INVALID_NAME, None, None), ], ) def test_CameraData_cameraByName(cameraHomeData, name, home, home_id, expected): - if home == INVALID_NAME or name == INVALID_NAME: + if home == INVALID_NAME or name == INVALID_NAME or home_id == "InvalidHomeID": assert cameraHomeData.cameraByName(name, home, home_id) is None elif home_id is None: assert cameraHomeData.cameraByName(name, home)["id"] == expected @@ -327,6 +328,9 @@ def test_CameraData_getHomeName(cameraHomeData): assert cameraHomeData.getHomeName(home_id) == "MYHOME" home_id = "91763b24c43d3e344f424e8c" assert cameraHomeData.getHomeName(home_id) == "Unknown" + home_id = "InvalidHomeID" + with pytest.raises(pyatmo.InvalidHome): + assert cameraHomeData.getHomeName(home_id) == "Unknown" def test_CameraData_gethomeId(cameraHomeData): diff --git a/tests/test_pyatmo_publicdata.py b/tests/test_pyatmo_publicdata.py index 47d7deb52..d16b88c11 100644 --- a/tests/test_pyatmo_publicdata.py +++ b/tests/test_pyatmo_publicdata.py @@ -23,7 +23,7 @@ def test_PublicData(auth, requests_mock): def test_PublicData_unavailable(auth, requests_mock): requests_mock.post(pyatmo.public_data._GETPUBLIC_DATA, status_code=404) - with pytest.raises(pyatmo.NoDevice): + with pytest.raises(pyatmo.ApiError): pyatmo.PublicData(auth) From a11512f113c1f5a2ceb96e0336dcebbe48b77dc8 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Mon, 30 Dec 2019 14:59:45 +0100 Subject: [PATCH 177/333] v3.2.0 Add Pipfile.lock to help manage dependencies Signed-off-by: Hugo D. (jabesq) --- Pipfile.lock | 627 +++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 Pipfile.lock diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 000000000..6ae498c4f --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,627 @@ +{ + "_meta": { + "hash": { + "sha256": "59d3e7d59518a230b6320797c1d5f9bbbcd6d7bc700c6475b345dde5935a1620" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + ], + "version": "==2019.11.28" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "oauthlib": { + "hashes": [ + "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", + "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" + ], + "version": "==3.1.0" + }, + "pyatmo": { + "editable": true, + "path": "." + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "index": "pypi", + "version": "==2.22.0" + }, + "requests-oauthlib": { + "hashes": [ + "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", + "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" + ], + "index": "pypi", + "version": "==1.3.0" + }, + "urllib3": { + "hashes": [ + "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", + "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" + ], + "version": "==1.25.7" + } + }, + "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "aspy.yaml": { + "hashes": [ + "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", + "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45" + ], + "version": "==1.3.0" + }, + "astroid": { + "hashes": [ + "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", + "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" + ], + "version": "==2.3.3" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "black": { + "hashes": [ + "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", + "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" + ], + "index": "pypi", + "version": "==19.10b0" + }, + "bleach": { + "hashes": [ + "sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16", + "sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa" + ], + "version": "==3.1.0" + }, + "certifi": { + "hashes": [ + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + ], + "version": "==2019.11.28" + }, + "cfgv": { + "hashes": [ + "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144", + "sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289" + ], + "version": "==2.0.1" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "coverage": { + "hashes": [ + "sha256:0101888bd1592a20ccadae081ba10e8b204d20235d18d05c6f7d5e904a38fc10", + "sha256:04b961862334687549eb91cd5178a6fbe977ad365bddc7c60f2227f2f9880cf4", + "sha256:1ca43dbd739c0fc30b0a3637a003a0d2c7edc1dd618359d58cc1e211742f8bd1", + "sha256:1cbb88b34187bdb841f2599770b7e6ff8e259dc3bb64fc7893acf44998acf5f8", + "sha256:232f0b52a5b978288f0bbc282a6c03fe48cd19a04202df44309919c142b3bb9c", + "sha256:24bcfa86fd9ce86b73a8368383c39d919c497a06eebb888b6f0c12f13e920b1a", + "sha256:25b8f60b5c7da71e64c18888f3067d5b6f1334b9681876b2fb41eea26de881ae", + "sha256:2714160a63da18aed9340c70ed514973971ee7e665e6b336917ff4cca81a25b1", + "sha256:2ca2cd5264e84b2cafc73f0045437f70c6378c0d7dbcddc9ee3fe192c1e29e5d", + "sha256:2cc707fc9aad2592fc686d63ef72dc0031fc98b6fb921d2f5395d9ab84fbc3ef", + "sha256:348630edea485f4228233c2f310a598abf8afa5f8c716c02a9698089687b6085", + "sha256:40fbfd6b044c9db13aeec1daf5887d322c710d811f944011757526ef6e323fd9", + "sha256:46c9c6a1d1190c0b75ec7c0f339088309952b82ae8d67a79ff1319eb4e749b96", + "sha256:591506e088901bdc25620c37aec885e82cc896528f28c57e113751e3471fc314", + "sha256:5ac71bba1e07eab403b082c4428f868c1c9e26a21041436b4905c4c3d4e49b08", + "sha256:5f622f19abda4e934938e24f1d67599249abc201844933a6f01aaa8663094489", + "sha256:65bead1ac8c8930cf92a1ccaedcce19a57298547d5d1db5c9d4d068a0675c38b", + "sha256:7362a7f829feda10c7265b553455de596b83d1623b3d436b6d3c51c688c57bf6", + "sha256:7f2675750c50151f806070ec11258edf4c328340916c53bac0adbc465abd6b1e", + "sha256:960d7f42277391e8b1c0b0ae427a214e1b31a1278de6b73f8807b20c2e913bba", + "sha256:a50b0888d8a021a3342d36a6086501e30de7d840ab68fca44913e97d14487dc1", + "sha256:b7dbc5e8c39ea3ad3db22715f1b5401cd698a621218680c6daf42c2f9d36e205", + "sha256:bb3d29df5d07d5399d58a394d0ef50adf303ab4fbf66dfd25b9ef258effcb692", + "sha256:c0fff2733f7c2950f58a4fd09b5db257b00c6fec57bf3f68c5bae004d804b407", + "sha256:c792d3707a86c01c02607ae74364854220fb3e82735f631cd0a345dea6b4cee5", + "sha256:c90bda74e16bcd03861b09b1d37c0a4158feda5d5a036bb2d6e58de6ff65793e", + "sha256:cfce79ce41cc1a1dc7fc85bb41eeeb32d34a4cf39a645c717c0550287e30ff06", + "sha256:eeafb646f374988c22c8e6da5ab9fb81367ecfe81c70c292623373d2a021b1a1", + "sha256:f425f50a6dd807cb9043d15a4fcfba3b5874a54d9587ccbb748899f70dc18c47", + "sha256:fcd4459fe35a400b8f416bc57906862693c9f88b66dc925e7f2a933e77f6b18b", + "sha256:ff3936dd5feaefb4f91c8c1f50a06c588b5dc69fba4f7d9c79a6617ad80bb7df" + ], + "version": "==5.0.1" + }, + "docutils": { + "hashes": [ + "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", + "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", + "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + ], + "index": "pypi", + "version": "==0.14" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "filelock": { + "hashes": [ + "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", + "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" + ], + "version": "==3.0.12" + }, + "flake8": { + "hashes": [ + "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", + "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" + ], + "index": "pypi", + "version": "==3.7.9" + }, + "freezegun": { + "hashes": [ + "sha256:2a4d9c8cd3c04a201e20c313caf8b6338f1cfa4cda43f46a94cc4a9fd13ea5e7", + "sha256:edfdf5bc6040969e6ed2e36eafe277963bdc8b7c01daeda96c5c8594576c9390" + ], + "index": "pypi", + "version": "==0.3.12" + }, + "identify": { + "hashes": [ + "sha256:6f44e637caa40d1b4cb37f6ed3b262ede74901d28b1cc5b1fc07360871edd65d", + "sha256:72e9c4ed3bc713c7045b762b0d2e2115c572b85abfc1f4604f5a4fd4c6642b71" + ], + "version": "==1.4.9" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "importlib-metadata": { + "hashes": [ + "sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45", + "sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f" + ], + "markers": "python_version < '3.8'", + "version": "==1.3.0" + }, + "isort": { + "hashes": [ + "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", + "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + ], + "index": "pypi", + "version": "==4.3.21" + }, + "keyring": { + "hashes": [ + "sha256:5f5f92327b6c7432bebc18a1b60cb3797d99b08db1f5b919b8187c37a01f1ccc", + "sha256:ad84f7fe26ab51731f089eaf1c44ebf4c5fae323653c908888a3a6212ad0bbe7" + ], + "version": "==21.0.0" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", + "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", + "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", + "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", + "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", + "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", + "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", + "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", + "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", + "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", + "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", + "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", + "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", + "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", + "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", + "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", + "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", + "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", + "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", + "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", + "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" + ], + "version": "==1.4.3" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", + "sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564" + ], + "version": "==8.0.2" + }, + "mypy": { + "hashes": [ + "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a", + "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7", + "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2", + "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474", + "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0", + "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217", + "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749", + "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6", + "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf", + "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36", + "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b", + "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72", + "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1", + "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1" + ], + "index": "pypi", + "version": "==0.761" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "nodeenv": { + "hashes": [ + "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" + ], + "version": "==1.3.3" + }, + "packaging": { + "hashes": [ + "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", + "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" + ], + "version": "==19.2" + }, + "pathspec": { + "hashes": [ + "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", + "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96" + ], + "version": "==0.7.0" + }, + "pkginfo": { + "hashes": [ + "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", + "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" + ], + "version": "==1.5.0.1" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "version": "==0.13.1" + }, + "pre-commit": { + "hashes": [ + "sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e", + "sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50" + ], + "index": "pypi", + "version": "==1.20.0" + }, + "py": { + "hashes": [ + "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", + "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" + ], + "version": "==1.8.1" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pyflakes": { + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, + "pygments": { + "hashes": [ + "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", + "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" + ], + "version": "==2.5.2" + }, + "pylint": { + "hashes": [ + "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", + "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" + ], + "index": "pypi", + "version": "==2.4.4" + }, + "pyparsing": { + "hashes": [ + "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", + "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + ], + "version": "==2.4.6" + }, + "pytest": { + "hashes": [ + "sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa", + "sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4" + ], + "index": "pypi", + "version": "==5.3.2" + }, + "pytest-cov": { + "hashes": [ + "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", + "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626" + ], + "index": "pypi", + "version": "==2.8.1" + }, + "pytest-mock": { + "hashes": [ + "sha256:67e414b3caef7bff6fc6bd83b22b5bc39147e4493f483c2679bc9d4dc485a94d", + "sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5" + ], + "index": "pypi", + "version": "==1.13.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" + ], + "version": "==2.8.1" + }, + "pyyaml": { + "hashes": [ + "sha256:21a8e19e2007a4047ffabbd8f0ee32c0dabae3b7f4b6c645110ae53e7714b470", + "sha256:74ad685bfb065f4bdd36d24aa97092f04bcbb1179b5ffdd3d5f994023fb8c292", + "sha256:79c3ba1da22e61c2a71aaa382c57518ab492278c8974c40187b900b50f3e0282", + "sha256:94ad913ab3fd967d14ecffda8182d7d0e1f7dd919b352773c492ec51890d3224", + "sha256:998db501e3a627c3e5678d6505f0e182d1529545df289db036cdc717f35d8058", + "sha256:9b69d4645bff5820713e8912bc61c4277dc127a6f8c197b52b6436503c42600f", + "sha256:9da13b536533518343a04f3c6564782ec8a13c705310b26b4832d77fa4d92a47", + "sha256:a76159f13b47fb44fb2acac8fef798a1940dd31b4acec6f4560bd11b2d92d31b", + "sha256:a9e9175c1e47a089a2b45d9e2afc6aae1f1f725538c32eec761894a42ba1227f", + "sha256:ea51ce7b96646ecd3bb12c2702e570c2bd7dd4d9f146db7fa83c5008ede35f66", + "sha256:ffbaaa05de60fc444eda3f6300d1af27d965b09b67f1fb4ebcc88dd0fb4ab1b4" + ], + "version": "==5.3b1" + }, + "readme-renderer": { + "hashes": [ + "sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f", + "sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d" + ], + "version": "==24.0" + }, + "regex": { + "hashes": [ + "sha256:032fdcc03406e1a6485ec09b826eac78732943840c4b29e503b789716f051d8d", + "sha256:0e6cf1e747f383f52a0964452658c04300a9a01e8a89c55ea22813931b580aa8", + "sha256:106e25a841921d8259dcef2a42786caae35bc750fb996f830065b3dfaa67b77e", + "sha256:1768cf42a78a11dae63152685e7a1d90af7a8d71d2d4f6d2387edea53a9e0588", + "sha256:27d1bd20d334f50b7ef078eba0f0756a640fd25f5f1708d3b5bed18a5d6bced9", + "sha256:29b20f66f2e044aafba86ecf10a84e611b4667643c42baa004247f5dfef4f90b", + "sha256:4850c78b53acf664a6578bba0e9ebeaf2807bb476c14ec7e0f936f2015133cae", + "sha256:57eacd38a5ec40ed7b19a968a9d01c0d977bda55664210be713e750dd7b33540", + "sha256:724eb24b92fc5fdc1501a1b4df44a68b9c1dda171c8ef8736799e903fb100f63", + "sha256:77ae8d926f38700432807ba293d768ba9e7652df0cbe76df2843b12f80f68885", + "sha256:78b3712ec529b2a71731fbb10b907b54d9c53a17ca589b42a578bc1e9a2c82ea", + "sha256:7bbbdbada3078dc360d4692a9b28479f569db7fc7f304b668787afc9feb38ec8", + "sha256:8d9ef7f6c403e35e73b7fc3cde9f6decdc43b1cb2ff8d058c53b9084bfcb553e", + "sha256:a83049eb717ae828ced9cf607845929efcb086a001fc8af93ff15c50012a5716", + "sha256:adc35d38952e688535980ae2109cad3a109520033642e759f987cf47fe278aa1", + "sha256:c29a77ad4463f71a506515d9ec3a899ed026b4b015bf43245c919ff36275444b", + "sha256:cfd31b3300fefa5eecb2fe596c6dee1b91b3a05ece9d5cfd2631afebf6c6fadd", + "sha256:d3ee0b035816e0520fac928de31b6572106f0d75597f6fa3206969a02baba06f", + "sha256:d508875793efdf6bab3d47850df8f40d4040ae9928d9d80864c1768d6aeaf8e3", + "sha256:ef0b828a7e22e58e06a1cceddba7b4665c6af8afeb22a0d8083001330572c147", + "sha256:faad39fdbe2c2ccda9846cd21581063086330efafa47d87afea4073a08128656" + ], + "version": "==2019.12.20" + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "index": "pypi", + "version": "==2.22.0" + }, + "requests-mock": { + "hashes": [ + "sha256:510df890afe08d36eca5bb16b4aa6308a6f85e3159ad3013bac8b9de7bd5a010", + "sha256:88d3402dd8b3c69a9e4f9d3a73ad11b15920c6efd36bc27bf1f701cf4a8e4646" + ], + "index": "pypi", + "version": "==1.7.0" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", + "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" + ], + "version": "==0.9.1" + }, + "six": { + "hashes": [ + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + ], + "version": "==1.13.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "tox": { + "hashes": [ + "sha256:06ba73b149bf838d5cd25dc30c2dd2671ae5b2757cf98e5c41a35fe449f131b3", + "sha256:806d0a9217584558cc93747a945a9d9bff10b141a5287f0c8429a08828a22192" + ], + "index": "pypi", + "version": "==3.14.3" + }, + "tqdm": { + "hashes": [ + "sha256:166a82cdea964ae45528e0cc89436255ff2be73dc848bdf239f13c501cae5dc7", + "sha256:9036904496bd2afacf836a6f206c5a766ce11d3e9319d54a4e794c0f34b111dc" + ], + "version": "==4.41.0" + }, + "twine": { + "hashes": [ + "sha256:c1af8ca391e43b0a06bbc155f7f67db0bf0d19d284bfc88d1675da497a946124", + "sha256:d561a5e511f70275e5a485a6275ff61851c16ffcb3a95a602189161112d9f160" + ], + "index": "pypi", + "version": "==3.1.1" + }, + "typed-ast": { + "hashes": [ + "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", + "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", + "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", + "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", + "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", + "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", + "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", + "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", + "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", + "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", + "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", + "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", + "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", + "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", + "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", + "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", + "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", + "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", + "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", + "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" + ], + "markers": "implementation_name == 'cpython' and python_version < '3.8'", + "version": "==1.4.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", + "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", + "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" + ], + "version": "==3.7.4.1" + }, + "urllib3": { + "hashes": [ + "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", + "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" + ], + "version": "==1.25.7" + }, + "virtualenv": { + "hashes": [ + "sha256:0d62c70883c0342d59c11d0ddac0d954d0431321a41ab20851facf2b222598f3", + "sha256:55059a7a676e4e19498f1aad09b8313a38fcc0cdbe4fdddc0e9b06946d21b4bb" + ], + "version": "==16.7.9" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" + }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + }, + "wrapt": { + "hashes": [ + "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" + ], + "version": "==1.11.2" + }, + "zipp": { + "hashes": [ + "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", + "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + ], + "version": "==0.6.0" + } + } +} diff --git a/setup.py b/setup.py index 21a85de52..f27cf6a44 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="pyatmo", - version="3.1.0", # Should be updated with new versions + version="3.2.0", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", packages=find_packages(exclude=["tests"], where="src"), From c47691a9de4be4cd80d13392f2411259bb7bfb8d Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 4 Jan 2020 23:08:34 +0100 Subject: [PATCH 178/333] Fix issue when a single station is unavailable (#98) --- src/pyatmo/weather_station.py | 3 +- tests/test_pyatmo_weatherstation.py | 51 ++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 79be749d5..df30eed1f 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -193,7 +193,8 @@ def lastData(self, station=None, exclude=0, byId=False): for st in stations: s = self.stationById(st) if byId else self.stationByName(st) if not s or "dashboard_data" not in s: - return None + LOG.info("Not dashboard data for station %s", st) + continue # Define oldest acceptable sensor measure event limit = (time.time() - exclude) if exclude else 0 ds = s["dashboard_data"] diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 507293117..c2e82676f 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -354,7 +354,7 @@ def test_WeatherStationData_monitoredConditions( "Yard", ], ), - ("NoValidStation", None, False, None), + ("NoValidStation", None, False, {}), ( None, 1000000, @@ -395,7 +395,7 @@ def test_WeatherStationData_lastData( if mod: assert sorted(mod) == expected else: - assert mod is expected + assert mod == expected @freeze_time("2019-06-11") @@ -414,8 +414,8 @@ def test_WeatherStationData_lastData( "12:34:56:37:11:ca", ], ), - ("", None, None,), - ("NoValidStation", None, None), + ("", None, {},), + ("NoValidStation", None, {},), ( "12:34:56:37:11:ca", 1000000, @@ -448,7 +448,7 @@ def test_WeatherStationData_lastData_byId( if mod: assert sorted(mod) == expected else: - assert mod is expected + assert mod == expected @freeze_time("2019-06-11") @@ -567,3 +567,44 @@ def test_WeatherStationData_lastData_measurements(weatherStationData): assert mod["NetatmoIndoor"]["max_temp"] == 25.6 assert mod["NetatmoIndoor"]["Temperature"] == 24.6 assert mod["NetatmoIndoor"]["Pressure"] == 1017.3 + + +@freeze_time("2019-06-11") +@pytest.mark.parametrize( + "station, exclude, expected", + [ + ( + "12:34:56:37:11:ca", + None, + [ + "12:34:56:03:1b:e4", + "12:34:56:05:51:20", + "12:34:56:07:bb:0e", + "12:34:56:07:bb:3e", + "12:34:56:36:fc:de", + "12:34:56:37:11:ca", + ], + ), + ( + None, + None, + [ + "12:34:56:03:1b:e4", + "12:34:56:05:51:20", + "12:34:56:07:bb:0e", + "12:34:56:07:bb:3e", + "12:34:56:36:fc:de", + "12:34:56:37:11:ca", + ], + ), + ("12:34:56:00:aa:01", None, {},), + ], +) +def test_WeatherStationData_lastData_bug_97( + weatherStationData, station, exclude, expected +): + mod = weatherStationData.lastData(station, exclude, byId=True) + if mod: + assert sorted(mod) == expected + else: + assert mod == expected From 8e84f521190ecf06504142e02c1f6ce331eb1233 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 12 Jan 2020 22:21:39 +0100 Subject: [PATCH 179/333] v3.2.1 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f27cf6a44..b0991a78c 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="pyatmo", - version="3.2.0", # Should be updated with new versions + version="3.2.1", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", packages=find_packages(exclude=["tests"], where="src"), From e99a3b8dad9b639e56233dfa5dfbf0539bdb0e6f Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Thu, 16 Jan 2020 23:04:09 +0100 Subject: [PATCH 180/333] v3.2.2 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b0991a78c..2f4599d9d 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="pyatmo", - version="3.2.1", # Should be updated with new versions + version="3.2.2", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", packages=find_packages(exclude=["tests"], where="src"), From 7c9a51a10cf3fb538bb0ae77a174e3b31c5ee4f9 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sun, 16 Feb 2020 13:37:08 +0100 Subject: [PATCH 181/333] Fix wind module issue (#101) * Fix wind module issue * Fix spaces in ids --- fixtures/weatherstation_data_simple.json | 4 ++-- src/pyatmo/helpers.py | 9 +++++++++ src/pyatmo/weather_station.py | 8 ++++---- tests/test_pyatmo_weatherstation.py | 12 ++++++++---- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/fixtures/weatherstation_data_simple.json b/fixtures/weatherstation_data_simple.json index d4b975103..8d377aedb 100644 --- a/fixtures/weatherstation_data_simple.json +++ b/fixtures/weatherstation_data_simple.json @@ -2,7 +2,7 @@ "body": { "devices": [ { - "_id": "12:34:56:37:11:ca", + "_id": "12:34 :56:37:11:ca", "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX", "date_setup": 1544558432, "last_setup": 1544558432, @@ -160,7 +160,7 @@ "battery_percent": 85 }, { - "_id": "12:34:56:05:51:20", + "_id": "12:34 :56:05:51:20", "type": "NAModule3", "module_name": "Yard", "data_type": [ diff --git a/src/pyatmo/helpers.py b/src/pyatmo/helpers.py index 43fa25ab1..5d7ab44f3 100644 --- a/src/pyatmo/helpers.py +++ b/src/pyatmo/helpers.py @@ -28,3 +28,12 @@ def toEpoch(value): def todayStamps(): today = timegm(time.strptime(time.strftime("%Y-%m-%d") + "GMT", "%Y-%m-%d%Z")) return today, today + 3600 * 24 + + +def fixId(rawData): + if rawData: + for station in rawData: + station["_id"] = station["_id"].replace(" ", "") + for module in station.get("modules", {}): + module["_id"] = module["_id"].replace(" ", "") + return rawData diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index df30eed1f..7c25fd117 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -2,7 +2,7 @@ import time from .exceptions import NoDevice -from .helpers import _BASE_URL, todayStamps +from .helpers import _BASE_URL, fixId, todayStamps LOG = logging.getLogger(__name__) @@ -25,7 +25,7 @@ def __init__(self, authData, urlReq=None): if resp is None or "body" not in resp: raise NoDevice("No weather station data returned by Netatmo server") try: - self.rawData = resp["body"].get("devices") + self.rawData = fixId(resp["body"].get("devices")) except KeyError: LOG.debug("No in response %s", resp) raise NoDevice("No weather station data returned by Netatmo server") @@ -79,7 +79,7 @@ def getModules(self, station=None, station_id=None): for s in stations: res[s["_id"]] = { "station_name": s["station_name"], - "module_name": s["module_name"], + "module_name": s.get("module_name", "module"), "id": s["_id"], } @@ -154,7 +154,7 @@ def monitoredConditions(self, module=None, moduleId=None): if cond == "Wind": # the Wind meter actually exposes the following conditions conditions.extend( - ["windangle", "windstrength", "gustangle", "guststrength"] + ["WindAngle", "WindStrength", "GustAngle", "GustStrength"] ) elif cond == "Rain": conditions.extend(["Rain", "sum_rain_24", "sum_rain_1"]) diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index c2e82676f..5428f3a56 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -257,14 +257,14 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): "Garden", None, [ + "GustAngle", + "GustStrength", + "WindAngle", + "WindStrength", "battery_percent", "battery_vp", - "gustangle", - "guststrength", "reachable", "rf_status", - "windangle", - "windstrength", ], ), ( @@ -567,6 +567,10 @@ def test_WeatherStationData_lastData_measurements(weatherStationData): assert mod["NetatmoIndoor"]["max_temp"] == 25.6 assert mod["NetatmoIndoor"]["Temperature"] == 24.6 assert mod["NetatmoIndoor"]["Pressure"] == 1017.3 + assert mod["Garden"]["WindAngle"] == 217 + assert mod["Garden"]["WindStrength"] == 4 + assert mod["Garden"]["GustAngle"] == 206 + assert mod["Garden"]["GustStrength"] == 9 @freeze_time("2019-06-11") From 5c5498e87fb03291ca40375ed3d4098adecbf25f Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 16 Feb 2020 13:54:07 +0100 Subject: [PATCH 182/333] Add instruction to build release Signed-off-by: Hugo D. (jabesq) --- .gitignore | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 1 + 2 files changed, 95 insertions(+) diff --git a/.gitignore b/.gitignore index f36d7d0d0..37437b539 100644 --- a/.gitignore +++ b/.gitignore @@ -220,3 +220,97 @@ dmypy.json !.vscode/extensions.json # End of https://www.gitignore.io/api/code + +# Created by https://www.gitignore.io/api/jetbrains +# Edit at https://www.gitignore.io/?templates=jetbrains + +### JetBrains ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/**/sonarlint/ + +# SonarQube Plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator/ + +# End of https://www.gitignore.io/api/jetbrains +.idea diff --git a/setup.py b/setup.py index 2f4599d9d..4324202ec 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ # python setup.py --dry-run --verbose install +# To build: python setup.py sdist bdist_wheel from setuptools import find_packages, setup From af51a0371ffd5cb09028f9092f5c4087461d2043 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 16 Feb 2020 13:56:19 +0100 Subject: [PATCH 183/333] v3.2.3 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4324202ec..4704d575d 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="pyatmo", - version="3.2.2", # Should be updated with new versions + version="3.2.3", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", packages=find_packages(exclude=["tests"], where="src"), From 8c4df8476816181f48bb3bc9951013cd5dfd25ca Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sun, 16 Feb 2020 14:57:07 +0100 Subject: [PATCH 184/333] Fix missing module name (#102) * Fix missing module issue * Remove try...except --- fixtures/weatherstation_data_simple.json | 91 +++++++++++++++++++++++- src/pyatmo/weather_station.py | 22 +++--- tests/test_pyatmo_weatherstation.py | 33 ++++++++- 3 files changed, 133 insertions(+), 13 deletions(-) diff --git a/fixtures/weatherstation_data_simple.json b/fixtures/weatherstation_data_simple.json index 8d377aedb..401633777 100644 --- a/fixtures/weatherstation_data_simple.json +++ b/fixtures/weatherstation_data_simple.json @@ -2,7 +2,7 @@ "body": { "devices": [ { - "_id": "12:34 :56:37:11:ca", + "_id": "12:34:56:37:11:ca", "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX", "date_setup": 1544558432, "last_setup": 1544558432, @@ -160,7 +160,7 @@ "battery_percent": 85 }, { - "_id": "12:34 :56:05:51:20", + "_id": "12:34:56:05:51:20", "type": "NAModule3", "module_name": "Yard", "data_type": [ @@ -182,6 +182,93 @@ "battery_percent": 93 } ] + }, + { + "_id": "12 :34: 56:36:fd:3c", + "station_name": "Valley Road", + "date_setup": 1545897146, + "last_setup": 1545897146, + "type": "NAMain", + "last_status_store": 1581835369, + "firmware": 137, + "last_upgrade": 1545897125, + "wifi_status": 53, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 69, + "city": "Valley", + "country": "AU", + "timezone": "Australia/Hobart", + "location": [ + 148.444226, + -41.721282 + ] + }, + "read_only": true, + "dashboard_data": { + "time_utc": 1581835330, + "Temperature": 22.4, + "CO2": 471, + "Humidity": 46, + "Noise": 47, + "Pressure": 1011.5, + "AbsolutePressure": 1002.8, + "min_temp": 18.1, + "max_temp": 22.5, + "date_max_temp": 1581829891, + "date_min_temp": 1581794878, + "temp_trend": "stable", + "pressure_trend": "stable" + }, + "modules": [ + { + "_id": "12 :34: 56:36:e6:c0", + "type": "NAModule1", + "module_name": "Module", + "data_type": [ + "Temperature", + "Humidity" + ], + "last_setup": 1545897146, + "battery_percent": 22, + "reachable": false, + "firmware": 46, + "last_message": 1572497781, + "last_seen": 1572497742, + "rf_status": 88, + "battery_vp": 4118 + }, + { + "_id": "12:34:56:05:25:6e", + "type": "NAModule3", + "module_name": "Rain Gauge", + "data_type": [ + "Rain" + ], + "last_setup": 1553997427, + "battery_percent": 82, + "reachable": true, + "firmware": 8, + "last_message": 1581835362, + "last_seen": 1581835354, + "rf_status": 78, + "battery_vp": 5594, + "dashboard_data": { + "time_utc": 1581835329, + "Rain": 0, + "sum_rain_1": 0, + "sum_rain_24": 0 + } + } + ] } ], "user": { diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 7c25fd117..2ea597d74 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -61,7 +61,7 @@ def modulesNamesList(self, station=None, station_id=None): else: res.update([m["module_name"] for m in self.modules.values()]) for s in self.stations.values(): - res.add(s["module_name"]) + res.add(s.get("module_name", "Station")) return list(res) def getModules(self, station=None, station_id=None): @@ -79,7 +79,7 @@ def getModules(self, station=None, station_id=None): for s in stations: res[s["_id"]] = { "station_name": s["station_name"], - "module_name": s.get("module_name", "module"), + "module_name": s.get("module_name", "Station"), "id": s["_id"], } @@ -104,24 +104,26 @@ def stationById(self, sid): """Return station by id.""" return None if sid not in self.stations else self.stations[sid] - def moduleByName(self, module, station=None): + def moduleByName(self, module_name, station=None): """Return module by name.""" s = None if station: s = self.stationByName(station) if not s: return None - if s["module_name"] == module: + if s["module_name"] == module_name: return s else: for s in self.stations.values(): - if "module_name" in s and s["module_name"] == module: - return s + if "module_name" in s: + if s["module_name"] == module_name: + return s + break 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 + module = self.modules[m] + if module["module_name"] == module_name: + if not s or module["main_device"] == s["_id"]: + return module return None def moduleById(self, mid, sid=None): diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 5428f3a56..b1d4852af 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -50,8 +50,11 @@ def test_WeatherStationData_no_data(auth, requests_mock): "Garden", "Kitchen", "Livingroom", + "Module", "NetatmoIndoor", "NetatmoOutdoor", + "Rain Gauge", + "Station", "Yard", ], ), @@ -115,6 +118,21 @@ def test_WeatherStationData_modulesNamesList(weatherStationData, station, expect "module_name": "NetatmoIndoor", "station_name": "MyStation", }, + "12:34:56:36:e6:c0": { + "id": "12:34:56:36:e6:c0", + "module_name": "Module", + "station_name": "Valley Road", + }, + "12:34:56:36:fd:3c": { + "id": "12:34:56:36:fd:3c", + "module_name": "Station", + "station_name": "Valley Road", + }, + "12:34:56:05:25:6e": { + "id": "12:34:56:05:25:6e", + "module_name": "Rain Gauge", + "station_name": "Valley Road", + }, }, ), ( @@ -365,6 +383,7 @@ def test_WeatherStationData_monitoredConditions( "Livingroom", "NetatmoIndoor", "NetatmoOutdoor", + "Rain Gauge", "Yard", ], ), @@ -372,7 +391,14 @@ def test_WeatherStationData_monitoredConditions( None, 798103, False, - ["Garden", "Kitchen", "NetatmoIndoor", "NetatmoOutdoor", "Yard"], + [ + "Garden", + "Kitchen", + "NetatmoIndoor", + "NetatmoOutdoor", + "Rain Gauge", + "Yard", + ], ), ( None, @@ -380,9 +406,11 @@ def test_WeatherStationData_monitoredConditions( True, [ "12:34:56:03:1b:e4", + "12:34:56:05:25:6e", "12:34:56:05:51:20", "12:34:56:07:bb:3e", "12:34:56:36:fc:de", + "12:34:56:36:fd:3c", "12:34:56:37:11:ca", ], ), @@ -531,6 +559,7 @@ def test_WeatherStationData_checkNotUpdated( "Livingroom", "NetatmoIndoor", "NetatmoOutdoor", + "Rain Gauge", "Yard", ], ), @@ -594,10 +623,12 @@ def test_WeatherStationData_lastData_measurements(weatherStationData): None, [ "12:34:56:03:1b:e4", + "12:34:56:05:25:6e", "12:34:56:05:51:20", "12:34:56:07:bb:0e", "12:34:56:07:bb:3e", "12:34:56:36:fc:de", + "12:34:56:36:fd:3c", "12:34:56:37:11:ca", ], ), From ebf985acb71e969c3a3d01e8db9cba990483fa42 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 19 Feb 2020 21:33:58 +0100 Subject: [PATCH 185/333] Fix crash when module name not given (#103) * Fix missing module issue * Remove try...except * Fix typo Co-authored-by: Hugo Dupras --- src/pyatmo/weather_station.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 2ea597d74..c8424b0fc 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -195,7 +195,7 @@ def lastData(self, station=None, exclude=0, byId=False): for st in stations: s = self.stationById(st) if byId else self.stationByName(st) if not s or "dashboard_data" not in s: - LOG.info("Not dashboard data for station %s", st) + LOG.info("No dashboard data for station %s", st) continue # Define oldest acceptable sensor measure event limit = (time.time() - exclude) if exclude else 0 From ca802746c84b4262d39139c19cf44f7b3e8e8efc Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Wed, 19 Feb 2020 21:39:39 +0100 Subject: [PATCH 186/333] v3.2.4 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4704d575d..2efda642e 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="pyatmo", - version="3.2.3", # Should be updated with new versions + version="3.2.4", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", packages=find_packages(exclude=["tests"], where="src"), From ab50c4e405226e4a4502d84de668b97118761fea Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Wed, 26 Feb 2020 22:43:55 +0100 Subject: [PATCH 187/333] Upgrade dependencies Signed-off-by: Hugo D. (jabesq) --- Pipfile.lock | 334 +++++++++++++++++++++++++-------------------------- 1 file changed, 167 insertions(+), 167 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 6ae498c4f..e119feff9 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -32,10 +32,10 @@ }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" ], - "version": "==2.8" + "version": "==2.9" }, "oauthlib": { "hashes": [ @@ -50,11 +50,11 @@ }, "requests": { "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" ], "index": "pypi", - "version": "==2.22.0" + "version": "==2.23.0" }, "requests-oauthlib": { "hashes": [ @@ -66,10 +66,10 @@ }, "urllib3": { "hashes": [ - "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", - "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" + "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", + "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" ], - "version": "==1.25.7" + "version": "==1.25.8" } }, "develop": { @@ -80,13 +80,6 @@ ], "version": "==1.4.3" }, - "aspy.yaml": { - "hashes": [ - "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", - "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45" - ], - "version": "==1.3.0" - }, "astroid": { "hashes": [ "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", @@ -111,10 +104,10 @@ }, "bleach": { "hashes": [ - "sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16", - "sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa" + "sha256:44f69771e2ac81ff30d929d485b7f9919f3ad6d019b6b20c74f3b8687c3f70df", + "sha256:aa8b870d0f46965bac2c073a93444636b0e1ca74e9777e34f03dd494b8a59d48" ], - "version": "==3.1.0" + "version": "==3.1.1" }, "certifi": { "hashes": [ @@ -125,10 +118,10 @@ }, "cfgv": { "hashes": [ - "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144", - "sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289" + "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53", + "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513" ], - "version": "==2.0.1" + "version": "==3.1.0" }, "chardet": { "hashes": [ @@ -146,39 +139,45 @@ }, "coverage": { "hashes": [ - "sha256:0101888bd1592a20ccadae081ba10e8b204d20235d18d05c6f7d5e904a38fc10", - "sha256:04b961862334687549eb91cd5178a6fbe977ad365bddc7c60f2227f2f9880cf4", - "sha256:1ca43dbd739c0fc30b0a3637a003a0d2c7edc1dd618359d58cc1e211742f8bd1", - "sha256:1cbb88b34187bdb841f2599770b7e6ff8e259dc3bb64fc7893acf44998acf5f8", - "sha256:232f0b52a5b978288f0bbc282a6c03fe48cd19a04202df44309919c142b3bb9c", - "sha256:24bcfa86fd9ce86b73a8368383c39d919c497a06eebb888b6f0c12f13e920b1a", - "sha256:25b8f60b5c7da71e64c18888f3067d5b6f1334b9681876b2fb41eea26de881ae", - "sha256:2714160a63da18aed9340c70ed514973971ee7e665e6b336917ff4cca81a25b1", - "sha256:2ca2cd5264e84b2cafc73f0045437f70c6378c0d7dbcddc9ee3fe192c1e29e5d", - "sha256:2cc707fc9aad2592fc686d63ef72dc0031fc98b6fb921d2f5395d9ab84fbc3ef", - "sha256:348630edea485f4228233c2f310a598abf8afa5f8c716c02a9698089687b6085", - "sha256:40fbfd6b044c9db13aeec1daf5887d322c710d811f944011757526ef6e323fd9", - "sha256:46c9c6a1d1190c0b75ec7c0f339088309952b82ae8d67a79ff1319eb4e749b96", - "sha256:591506e088901bdc25620c37aec885e82cc896528f28c57e113751e3471fc314", - "sha256:5ac71bba1e07eab403b082c4428f868c1c9e26a21041436b4905c4c3d4e49b08", - "sha256:5f622f19abda4e934938e24f1d67599249abc201844933a6f01aaa8663094489", - "sha256:65bead1ac8c8930cf92a1ccaedcce19a57298547d5d1db5c9d4d068a0675c38b", - "sha256:7362a7f829feda10c7265b553455de596b83d1623b3d436b6d3c51c688c57bf6", - "sha256:7f2675750c50151f806070ec11258edf4c328340916c53bac0adbc465abd6b1e", - "sha256:960d7f42277391e8b1c0b0ae427a214e1b31a1278de6b73f8807b20c2e913bba", - "sha256:a50b0888d8a021a3342d36a6086501e30de7d840ab68fca44913e97d14487dc1", - "sha256:b7dbc5e8c39ea3ad3db22715f1b5401cd698a621218680c6daf42c2f9d36e205", - "sha256:bb3d29df5d07d5399d58a394d0ef50adf303ab4fbf66dfd25b9ef258effcb692", - "sha256:c0fff2733f7c2950f58a4fd09b5db257b00c6fec57bf3f68c5bae004d804b407", - "sha256:c792d3707a86c01c02607ae74364854220fb3e82735f631cd0a345dea6b4cee5", - "sha256:c90bda74e16bcd03861b09b1d37c0a4158feda5d5a036bb2d6e58de6ff65793e", - "sha256:cfce79ce41cc1a1dc7fc85bb41eeeb32d34a4cf39a645c717c0550287e30ff06", - "sha256:eeafb646f374988c22c8e6da5ab9fb81367ecfe81c70c292623373d2a021b1a1", - "sha256:f425f50a6dd807cb9043d15a4fcfba3b5874a54d9587ccbb748899f70dc18c47", - "sha256:fcd4459fe35a400b8f416bc57906862693c9f88b66dc925e7f2a933e77f6b18b", - "sha256:ff3936dd5feaefb4f91c8c1f50a06c588b5dc69fba4f7d9c79a6617ad80bb7df" - ], - "version": "==5.0.1" + "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3", + "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c", + "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0", + "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477", + "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a", + "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf", + "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691", + "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73", + "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987", + "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894", + "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e", + "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef", + "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf", + "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68", + "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8", + "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954", + "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2", + "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40", + "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc", + "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc", + "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e", + "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d", + "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f", + "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc", + "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301", + "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea", + "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb", + "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af", + "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52", + "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37", + "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0" + ], + "version": "==5.0.3" + }, + "distlib": { + "hashes": [ + "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21" + ], + "version": "==0.3.0" }, "docutils": { "hashes": [ @@ -213,33 +212,33 @@ }, "freezegun": { "hashes": [ - "sha256:2a4d9c8cd3c04a201e20c313caf8b6338f1cfa4cda43f46a94cc4a9fd13ea5e7", - "sha256:edfdf5bc6040969e6ed2e36eafe277963bdc8b7c01daeda96c5c8594576c9390" + "sha256:82c757a05b7c7ca3e176bfebd7d6779fd9139c7cb4ef969c38a28d74deef89b2", + "sha256:e2062f2c7f95cc276a834c22f1a17179467176b624cc6f936e8bc3be5535ad1b" ], "index": "pypi", - "version": "==0.3.12" + "version": "==0.3.15" }, "identify": { "hashes": [ - "sha256:6f44e637caa40d1b4cb37f6ed3b262ede74901d28b1cc5b1fc07360871edd65d", - "sha256:72e9c4ed3bc713c7045b762b0d2e2115c572b85abfc1f4604f5a4fd4c6642b71" + "sha256:1222b648251bdcb8deb240b294f450fbf704c7984e08baa92507e4ea10b436d5", + "sha256:d824ebe21f38325c771c41b08a95a761db1982f1fc0eee37c6c97df3f1636b96" ], - "version": "==1.4.9" + "version": "==1.4.11" }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" ], - "version": "==2.8" + "version": "==2.9" }, "importlib-metadata": { "hashes": [ - "sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45", - "sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f" + "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302", + "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b" ], "markers": "python_version < '3.8'", - "version": "==1.3.0" + "version": "==1.5.0" }, "isort": { "hashes": [ @@ -251,10 +250,10 @@ }, "keyring": { "hashes": [ - "sha256:5f5f92327b6c7432bebc18a1b60cb3797d99b08db1f5b919b8187c37a01f1ccc", - "sha256:ad84f7fe26ab51731f089eaf1c44ebf4c5fae323653c908888a3a6212ad0bbe7" + "sha256:1f393f7466314068961c7e1d508120c092bd71fa54e3d93b76180b526d4abc56", + "sha256:24ae23ab2d6adc59138339e56843e33ec7b0a6b2f06302662477085c6c0aca00" ], - "version": "==21.0.0" + "version": "==21.1.0" }, "lazy-object-proxy": { "hashes": [ @@ -291,10 +290,10 @@ }, "more-itertools": { "hashes": [ - "sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", - "sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564" + "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", + "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" ], - "version": "==8.0.2" + "version": "==8.2.0" }, "mypy": { "hashes": [ @@ -325,16 +324,16 @@ }, "nodeenv": { "hashes": [ - "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" + "sha256:5b2438f2e42af54ca968dd1b374d14a1194848955187b0e5e4be1f73813a5212" ], - "version": "==1.3.3" + "version": "==1.3.5" }, "packaging": { "hashes": [ - "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", - "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" + "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73", + "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334" ], - "version": "==19.2" + "version": "==20.1" }, "pathspec": { "hashes": [ @@ -359,11 +358,11 @@ }, "pre-commit": { "hashes": [ - "sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e", - "sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50" + "sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6", + "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb" ], "index": "pypi", - "version": "==1.20.0" + "version": "==2.1.1" }, "py": { "hashes": [ @@ -410,11 +409,11 @@ }, "pytest": { "hashes": [ - "sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa", - "sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4" + "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d", + "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6" ], "index": "pypi", - "version": "==5.3.2" + "version": "==5.3.5" }, "pytest-cov": { "hashes": [ @@ -426,11 +425,11 @@ }, "pytest-mock": { "hashes": [ - "sha256:67e414b3caef7bff6fc6bd83b22b5bc39147e4493f483c2679bc9d4dc485a94d", - "sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5" + "sha256:b35eb281e93aafed138db25c8772b95d3756108b601947f89af503f8c629413f", + "sha256:cb67402d87d5f53c579263d37971a164743dc33c159dfb4fb4a86f37c5552307" ], "index": "pypi", - "version": "==1.13.0" + "version": "==2.0.0" }, "python-dateutil": { "hashes": [ @@ -441,19 +440,19 @@ }, "pyyaml": { "hashes": [ - "sha256:21a8e19e2007a4047ffabbd8f0ee32c0dabae3b7f4b6c645110ae53e7714b470", - "sha256:74ad685bfb065f4bdd36d24aa97092f04bcbb1179b5ffdd3d5f994023fb8c292", - "sha256:79c3ba1da22e61c2a71aaa382c57518ab492278c8974c40187b900b50f3e0282", - "sha256:94ad913ab3fd967d14ecffda8182d7d0e1f7dd919b352773c492ec51890d3224", - "sha256:998db501e3a627c3e5678d6505f0e182d1529545df289db036cdc717f35d8058", - "sha256:9b69d4645bff5820713e8912bc61c4277dc127a6f8c197b52b6436503c42600f", - "sha256:9da13b536533518343a04f3c6564782ec8a13c705310b26b4832d77fa4d92a47", - "sha256:a76159f13b47fb44fb2acac8fef798a1940dd31b4acec6f4560bd11b2d92d31b", - "sha256:a9e9175c1e47a089a2b45d9e2afc6aae1f1f725538c32eec761894a42ba1227f", - "sha256:ea51ce7b96646ecd3bb12c2702e570c2bd7dd4d9f146db7fa83c5008ede35f66", - "sha256:ffbaaa05de60fc444eda3f6300d1af27d965b09b67f1fb4ebcc88dd0fb4ab1b4" + "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", + "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", + "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", + "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", + "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", + "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", + "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", + "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", + "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", + "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", + "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" ], - "version": "==5.3b1" + "version": "==5.3" }, "readme-renderer": { "hashes": [ @@ -464,37 +463,37 @@ }, "regex": { "hashes": [ - "sha256:032fdcc03406e1a6485ec09b826eac78732943840c4b29e503b789716f051d8d", - "sha256:0e6cf1e747f383f52a0964452658c04300a9a01e8a89c55ea22813931b580aa8", - "sha256:106e25a841921d8259dcef2a42786caae35bc750fb996f830065b3dfaa67b77e", - "sha256:1768cf42a78a11dae63152685e7a1d90af7a8d71d2d4f6d2387edea53a9e0588", - "sha256:27d1bd20d334f50b7ef078eba0f0756a640fd25f5f1708d3b5bed18a5d6bced9", - "sha256:29b20f66f2e044aafba86ecf10a84e611b4667643c42baa004247f5dfef4f90b", - "sha256:4850c78b53acf664a6578bba0e9ebeaf2807bb476c14ec7e0f936f2015133cae", - "sha256:57eacd38a5ec40ed7b19a968a9d01c0d977bda55664210be713e750dd7b33540", - "sha256:724eb24b92fc5fdc1501a1b4df44a68b9c1dda171c8ef8736799e903fb100f63", - "sha256:77ae8d926f38700432807ba293d768ba9e7652df0cbe76df2843b12f80f68885", - "sha256:78b3712ec529b2a71731fbb10b907b54d9c53a17ca589b42a578bc1e9a2c82ea", - "sha256:7bbbdbada3078dc360d4692a9b28479f569db7fc7f304b668787afc9feb38ec8", - "sha256:8d9ef7f6c403e35e73b7fc3cde9f6decdc43b1cb2ff8d058c53b9084bfcb553e", - "sha256:a83049eb717ae828ced9cf607845929efcb086a001fc8af93ff15c50012a5716", - "sha256:adc35d38952e688535980ae2109cad3a109520033642e759f987cf47fe278aa1", - "sha256:c29a77ad4463f71a506515d9ec3a899ed026b4b015bf43245c919ff36275444b", - "sha256:cfd31b3300fefa5eecb2fe596c6dee1b91b3a05ece9d5cfd2631afebf6c6fadd", - "sha256:d3ee0b035816e0520fac928de31b6572106f0d75597f6fa3206969a02baba06f", - "sha256:d508875793efdf6bab3d47850df8f40d4040ae9928d9d80864c1768d6aeaf8e3", - "sha256:ef0b828a7e22e58e06a1cceddba7b4665c6af8afeb22a0d8083001330572c147", - "sha256:faad39fdbe2c2ccda9846cd21581063086330efafa47d87afea4073a08128656" - ], - "version": "==2019.12.20" + "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431", + "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242", + "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1", + "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d", + "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045", + "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b", + "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400", + "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa", + "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0", + "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69", + "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74", + "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb", + "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26", + "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5", + "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2", + "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce", + "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab", + "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e", + "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70", + "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc", + "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0" + ], + "version": "==2020.2.20" }, "requests": { "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" ], "index": "pypi", - "version": "==2.22.0" + "version": "==2.23.0" }, "requests-mock": { "hashes": [ @@ -513,10 +512,10 @@ }, "six": { "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" ], - "version": "==1.13.0" + "version": "==1.14.0" }, "toml": { "hashes": [ @@ -527,18 +526,18 @@ }, "tox": { "hashes": [ - "sha256:06ba73b149bf838d5cd25dc30c2dd2671ae5b2757cf98e5c41a35fe449f131b3", - "sha256:806d0a9217584558cc93747a945a9d9bff10b141a5287f0c8429a08828a22192" + "sha256:0cbe98369081fa16bd6f1163d3d0b2a62afa29d402ccfad2bd09fb2668be0956", + "sha256:676f1e3e7de245ad870f956436b84ea226210587d1f72c8dfb8cd5ac7b6f0e70" ], "index": "pypi", - "version": "==3.14.3" + "version": "==3.14.5" }, "tqdm": { "hashes": [ - "sha256:166a82cdea964ae45528e0cc89436255ff2be73dc848bdf239f13c501cae5dc7", - "sha256:9036904496bd2afacf836a6f206c5a766ce11d3e9319d54a4e794c0f34b111dc" + "sha256:0d8b5afb66e23d80433102e9bd8b5c8b65d34c2a2255b2de58d97bd2ea8170fd", + "sha256:f35fb121bafa030bd94e74fcfd44f3c2830039a2ddef7fc87ef1c2d205237b24" ], - "version": "==4.41.0" + "version": "==4.43.0" }, "twine": { "hashes": [ @@ -550,29 +549,30 @@ }, "typed-ast": { "hashes": [ - "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", - "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", - "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", - "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", - "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", - "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", - "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", - "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", - "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", - "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", - "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", - "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", - "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", - "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", - "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", - "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", - "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", - "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", - "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", - "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], "markers": "implementation_name == 'cpython' and python_version < '3.8'", - "version": "==1.4.0" + "version": "==1.4.1" }, "typing-extensions": { "hashes": [ @@ -584,24 +584,24 @@ }, "urllib3": { "hashes": [ - "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", - "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" + "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", + "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" ], - "version": "==1.25.7" + "version": "==1.25.8" }, "virtualenv": { "hashes": [ - "sha256:0d62c70883c0342d59c11d0ddac0d954d0431321a41ab20851facf2b222598f3", - "sha256:55059a7a676e4e19498f1aad09b8313a38fcc0cdbe4fdddc0e9b06946d21b4bb" + "sha256:30ea90b21dabd11da5f509710ad3be2ae47d40ccbc717dfdd2efe4367c10f598", + "sha256:4a36a96d785428278edd389d9c36d763c5755844beb7509279194647b1ef47f1" ], - "version": "==16.7.9" + "version": "==20.0.7" }, "wcwidth": { "hashes": [ - "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", - "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", + "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" ], - "version": "==0.1.7" + "version": "==0.1.8" }, "webencodings": { "hashes": [ @@ -618,10 +618,10 @@ }, "zipp": { "hashes": [ - "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", - "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + "sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2", + "sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a" ], - "version": "==0.6.0" + "version": "==3.0.0" } } } From 6d965f5e6d9d382e6c679989a0603053029d17bf Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 26 Feb 2020 22:46:46 +0100 Subject: [PATCH 188/333] Add new parameters to setroomthermpoint and setthermmode (#104) --- ...home_status_error_invalid_schedule_id.json | 6 + src/pyatmo/thermostat.py | 12 +- tests/test_pyatmo_thermostat.py | 105 ++++++++++++++++-- 3 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 fixtures/home_status_error_invalid_schedule_id.json diff --git a/fixtures/home_status_error_invalid_schedule_id.json b/fixtures/home_status_error_invalid_schedule_id.json new file mode 100644 index 000000000..1ff011bf9 --- /dev/null +++ b/fixtures/home_status_error_invalid_schedule_id.json @@ -0,0 +1,6 @@ +{ + "error": { + "code": 21, + "message": "schedule is not therm schedule" + } +} \ No newline at end of file diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index a59e92506..38931f2be 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -341,19 +341,27 @@ def thermostatType(self, home, rid, home_id=None): if module_id in self.valves: return "NRV" - def setThermmode(self, home_id, mode): + def setThermmode(self, home_id, mode, end_time=None, schedule_id=None): postParams = { "home_id": home_id, "mode": mode, } + if end_time is not None and mode in ("hg", "away"): + postParams["endtime"] = end_time + if schedule_id is not None and mode == "schedule": + postParams["schedule_id"] = schedule_id return self.authData.post_request(url=_SETTHERMMODE_REQ, params=postParams) - def setroomThermpoint(self, home_id, room_id, mode, temp=None): + def setroomThermpoint(self, home_id, room_id, mode, temp=None, end_time=None): postParams = { "home_id": home_id, "room_id": room_id, "mode": mode, } + # Temp and endtime should only be send when mode=='manual', but netatmo api can + # handle that even when mode == 'home' and these settings don't make sense if temp is not None: postParams["temp"] = temp + if end_time is not None: + postParams["endtime"] = end_time return self.authData.post_request(url=_SETROOMTHERMPOINT_REQ, params=postParams) diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index 4f8509831..7c40a8afe 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -332,21 +332,78 @@ def test_HomeStatus_thermostatType(homeStatus): @pytest.mark.parametrize( - "home_id, mode, json_fixture, expected", + "home_id, mode, end_time, schedule_id, json_fixture, expected", [ - (None, None, "home_status_error_mode_is_missing.json", "mode is missing"), + ( + None, + None, + None, + None, + "home_status_error_mode_is_missing.json", + "mode is missing", + ), ( "91763b24c43d3e344f424e8b", None, + None, + None, "home_status_error_mode_is_missing.json", "mode is missing", ), - ("invalidID", "away", "home_status_error_invalid_id.json", "Invalid id"), - ("91763b24c43d3e344f424e8b", "away", "status_ok.json", "ok"), + ( + "invalidID", + "away", + None, + None, + "home_status_error_invalid_id.json", + "Invalid id", + ), + ("91763b24c43d3e344f424e8b", "away", None, None, "status_ok.json", "ok"), + ("91763b24c43d3e344f424e8b", "away", 1559162650, None, "status_ok.json", "ok"), + ( + "91763b24c43d3e344f424e8b", + "away", + 1559162650, + 0000000, + "status_ok.json", + "ok", + ), + ( + "91763b24c43d3e344f424e8b", + "schedule", + None, + "591b54a2764ff4d50d8b5795", + "status_ok.json", + "ok", + ), + ( + "91763b24c43d3e344f424e8b", + "schedule", + 1559162650, + "591b54a2764ff4d50d8b5795", + "status_ok.json", + "ok", + ), + ( + "91763b24c43d3e344f424e8b", + "schedule", + None, + "blahblahblah", + "home_status_error_invalid_schedule_id.json", + "schedule is not therm schedule", + ), ], ) def test_HomeData_setThermmode( - homeStatus, requests_mock, caplog, home_id, mode, json_fixture, expected + homeStatus, + requests_mock, + caplog, + home_id, + mode, + end_time, + schedule_id, + json_fixture, + expected, ): with open("fixtures/%s" % json_fixture) as f: json_fixture = json.load(f) @@ -355,7 +412,9 @@ def test_HomeData_setThermmode( json=json_fixture, headers={"content-type": "application/json"}, ) - res = homeStatus.setThermmode(home_id=home_id, mode=mode) + res = homeStatus.setThermmode( + home_id=home_id, mode=mode, end_time=end_time, schedule_id=schedule_id + ) if "error" in res: assert expected in res["error"]["message"] else: @@ -363,14 +422,41 @@ def test_HomeData_setThermmode( @pytest.mark.parametrize( - "home_id, room_id, mode, temp, json_fixture, expected", + "home_id, room_id, mode, temp, end_time, json_fixture, expected", [ - ("91763b24c43d3e344f424e8b", "2746182631", "home", 14, "status_ok.json", "ok"), + ( + "91763b24c43d3e344f424e8b", + "2746182631", + "home", + 14, + None, + "status_ok.json", + "ok", + ), + ( + "91763b24c43d3e344f424e8b", + "2746182631", + "home", + 14, + 1559162650, + "status_ok.json", + "ok", + ), ( "91763b24c43d3e344f424e8b", "2746182631", "home", None, + None, + "status_ok.json", + "ok", + ), + ( + "91763b24c43d3e344f424e8b", + "2746182631", + "home", + None, + 1559162650, "status_ok.json", "ok", ), @@ -384,6 +470,7 @@ def test_HomeData_setroomThermpoint( room_id, mode, temp, + end_time, json_fixture, expected, ): @@ -396,7 +483,7 @@ def test_HomeData_setroomThermpoint( ) assert ( homeStatus.setroomThermpoint( - home_id=home_id, room_id=room_id, mode=mode, temp=temp + home_id=home_id, room_id=room_id, mode=mode, temp=temp, end_time=end_time )["status"] == expected ) From 175513c6e39c7c053a9fe1016db4f61b972295cb Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 26 Feb 2020 22:55:29 +0100 Subject: [PATCH 189/333] Fix webhook registration (#105) * Introduce retry handling * Set eventList even when API call fails * Handle timeout and ssl related errors * Catch requests exceptions * Evaluate scope if token is given * Log error if response is None Co-authored-by: Hugo Dupras --- src/pyatmo/auth.py | 24 +++++++++++++++++------- src/pyatmo/camera.py | 8 ++++++-- tests/test_pyatmo.py | 8 -------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index ad1ae2654..db2aaf96b 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -1,4 +1,5 @@ import logging +from time import sleep from typing import Callable, Dict, Optional, Tuple, Union import requests @@ -68,7 +69,10 @@ def __init__( self.client_secret = client_secret self.redirect_uri = redirect_uri self.token_updater = token_updater - self.scope = " ".join(ALL_SCOPES) if not scope else scope + if token: + self.scope = " ".join(token["scope"]) + else: + self.scope = " ".join(ALL_SCOPES) if not scope else scope self.extra = { "client_id": self.client_id, @@ -93,7 +97,7 @@ def refresh_tokens(self) -> Dict[str, Union[str, int]]: return token def post_request( - self, url: str, params: Optional[Dict[str, str]] = None, timeout: int = 30 + self, url: str, params: Optional[Dict[str, str]] = None, timeout: int = 5 ): """Wrapper for post requests.""" if not params: @@ -112,16 +116,22 @@ def query(url, params, timeout, retries): return try: return self._oauth.post(url=url, data=params, timeout=timeout) - except TokenExpiredError: + except ( + TokenExpiredError, + requests.exceptions.ReadTimeout, + requests.exceptions.ConnectionError, + ): self._oauth.token = self.refresh_tokens() + # Sleep for 1 sec to prevent authentication related + # timeouts after a token refresh. + sleep(1) return query(url, params, timeout, retries - 1) resp = query(url, params, timeout, 3) - if not resp: - raise ApiError(f"Error when accessing '{url}'") - - if not resp.ok: + if resp is None: + LOG.debug("Resp is None - %s", resp) + elif not resp.ok: LOG.debug("The Netatmo API returned %s", resp.status_code) LOG.debug("Netato API error: %s", resp.content) if resp.status_code == 404: diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index f4d7dde1f..29de080c5 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -2,6 +2,8 @@ import time from typing import Dict, Tuple +from requests.exceptions import ReadTimeout + from .exceptions import ApiError, InvalidHome, NoDevice from .helpers import _BASE_URL, LOG @@ -272,7 +274,8 @@ def check_url(url): return None try: resp = self.authData.post_request(url=f"{url}/command/ping") - except ApiError: + except (ApiError, ReadTimeout): + LOG.debug("Timeout validation the camera url %s", url) return None else: return resp.get("local_url") @@ -447,6 +450,8 @@ def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): "home_id": home_id, "event_id": event["id"], } + + eventList = [] try: resp = self.authData.post_request( url=_GETEVENTSUNTIL_REQ, params=postParams @@ -457,7 +462,6 @@ def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): except KeyError: LOG.debug("eventList response: %s", resp) LOG.debug("eventList body: %s", resp["body"]) - eventList = [] for e in eventList: if e["type"] == "outdoor": diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index 892811e20..18d03c5d4 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -3,7 +3,6 @@ import time import pytest -import requests import oauthlib import pyatmo @@ -69,13 +68,6 @@ def test_postRequest_fail(auth, requests_mock, test_input, expected): resp = auth.post_request(pyatmo.helpers._BASE_URL, None) -def test_postRequest_timeout(auth, requests_mock): - """Test failing requests against the Netatmo API with timeouts.""" - requests_mock.post(pyatmo.helpers._BASE_URL, exc=requests.exceptions.ConnectTimeout) - with pytest.raises(requests.exceptions.ConnectTimeout): - assert auth.post_request(pyatmo.helpers._BASE_URL, None) - - @pytest.mark.parametrize( "test_input,expected", [ From 3506add47268ae28ff496be5a0865f69be38cc04 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 28 Feb 2020 07:17:37 +0100 Subject: [PATCH 190/333] Add set_state --- src/pyatmo/auth.py | 14 +++++++++++++- src/pyatmo/camera.py | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index db2aaf96b..f7129ed1c 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -103,6 +103,11 @@ def post_request( if not params: params = {} + if "json" in params: + json_params = params.pop("json") + else: + json_params = None + if "http://" in url: try: resp = requests.post(url, data=params, timeout=timeout) @@ -115,7 +120,14 @@ def query(url, params, timeout, retries): LOG.error("Too many retries") return try: - return self._oauth.post(url=url, data=params, timeout=timeout) + if json_params: + rsp = self._oauth.post( + url=url, json=json_params, timeout=timeout + ) + else: + rsp = self._oauth.post(url=url, data=params, timeout=timeout) + + return rsp except ( TokenExpiredError, requests.exceptions.ReadTimeout, diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 29de080c5..b88932dc5 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -790,3 +790,30 @@ def module_opened(self, mid, cid, exclude=0): ): return True return False + + def set_state(self, home_id: str, camera_id: str, state: Tuple[str, str]) -> bool: + """Turn camera on/off. + + Arguments: + home_id {str} -- ID of a home + camera_id {str} -- ID of a camera + + Returns: + Boolean -- Success of the request + """ + param, val = state + + postParams = { + "json": { + "home": {"id": home_id, "modules": [{"id": camera_id, param: val}]} + }, + } + + resp = self.authData.post_request(url=_SETSTATE_REQ, params=postParams) + + if "error" in resp: + LOG.debug("%s", resp) + return False + + LOG.debug("%s", resp) + return True From f1f66699d52ec9a548cc0bb39cffadfe974ecf71 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 28 Feb 2020 07:58:34 +0100 Subject: [PATCH 191/333] Make parameters explicit --- src/pyatmo/camera.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index b88932dc5..8681456f9 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -791,17 +791,31 @@ def module_opened(self, mid, cid, exclude=0): return True return False - def set_state(self, home_id: str, camera_id: str, state: Tuple[str, str]) -> bool: + def set_state( + self, + home_id: str, + camera_id: str, + floodlight: str = None, + monitoring: str = None, + ) -> bool: """Turn camera on/off. Arguments: home_id {str} -- ID of a home camera_id {str} -- ID of a camera + floodlight {str} -- Mode for floodlight (on/auto) + monitoring {str} -- Mode for monitoring (on/off) Returns: Boolean -- Success of the request """ - param, val = state + if floodlight and monitoring: + LOG.error("You can only set on of either states") + return False + elif floodlight: + param, val = "floodlight", floodlight.lower() + elif monitoring: + param, val = "monitoring", monitoring.lower() postParams = { "json": { @@ -809,7 +823,11 @@ def set_state(self, home_id: str, camera_id: str, state: Tuple[str, str]) -> boo }, } - resp = self.authData.post_request(url=_SETSTATE_REQ, params=postParams) + try: + resp = self.authData.post_request(url=_SETSTATE_REQ, params=postParams) + except ApiError as err_msg: + LOG.error(err_msg) + return False if "error" in resp: LOG.debug("%s", resp) From 8a0bb21c47951743761c8628b1aeba6512279e45 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 28 Feb 2020 12:53:53 +0100 Subject: [PATCH 192/333] Add missing scope --- src/pyatmo/auth.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index f7129ed1c..266d64777 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -26,6 +26,7 @@ "write_camera", "read_presence", "access_presence", + "write_presence", "read_homecoach", "read_smokedetector", "read_thermostat", From 9f8867844d0021affcd98b8c26c48f7c079e798d Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 28 Feb 2020 12:55:09 +0100 Subject: [PATCH 193/333] Chek values --- src/pyatmo/camera.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 8681456f9..b82e04a44 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -814,8 +814,14 @@ def set_state( return False elif floodlight: param, val = "floodlight", floodlight.lower() + if val not in ["on", "auto"]: + LOG.error("Invalid value fÃŧr floodlight") + return False elif monitoring: param, val = "monitoring", monitoring.lower() + if val not in ["on", "off"]: + LOG.error("Invalid value fÃŧr monitoring") + return False postParams = { "json": { @@ -826,7 +832,7 @@ def set_state( try: resp = self.authData.post_request(url=_SETSTATE_REQ, params=postParams) except ApiError as err_msg: - LOG.error(err_msg) + LOG.error("%s", err_msg) return False if "error" in resp: From 88764f9dd28bbe4541eba1c2ea7cdc9cf3ef2e88 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 29 Feb 2020 00:31:14 +0100 Subject: [PATCH 194/333] Enable multiple modes in one call --- src/pyatmo/camera.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index b82e04a44..68bb415e5 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -809,24 +809,24 @@ def set_state( Returns: Boolean -- Success of the request """ - if floodlight and monitoring: - LOG.error("You can only set on of either states") - return False - elif floodlight: + module = {"id": camera_id} + + if floodlight: param, val = "floodlight", floodlight.lower() if val not in ["on", "auto"]: LOG.error("Invalid value fÃŧr floodlight") - return False - elif monitoring: + else: + module[param] = val + + if monitoring: param, val = "monitoring", monitoring.lower() if val not in ["on", "off"]: LOG.error("Invalid value fÃŧr monitoring") - return False + else: + module[param] = val postParams = { - "json": { - "home": {"id": home_id, "modules": [{"id": camera_id, param: val}]} - }, + "json": {"home": {"id": home_id, "modules": [module]}}, } try: @@ -840,4 +840,5 @@ def set_state( return False LOG.debug("%s", resp) + print(resp) return True From 7c3fa32baf5c422dbd47bbc70433dd7ac6b9a39c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 29 Feb 2020 00:35:28 +0100 Subject: [PATCH 195/333] Clean up print statement --- src/pyatmo/camera.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 68bb415e5..65bee6c56 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -840,5 +840,4 @@ def set_state( return False LOG.debug("%s", resp) - print(resp) return True From bb6576579864dfd8eb55e93f3e9e7ef210adcad2 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 29 Feb 2020 09:48:34 +0100 Subject: [PATCH 196/333] Add basic test for set_state --- fixtures/camera_set_state_ok.json | 4 +++ tests/test_pyatmo_camera.py | 42 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 fixtures/camera_set_state_ok.json diff --git a/fixtures/camera_set_state_ok.json b/fixtures/camera_set_state_ok.json new file mode 100644 index 000000000..0885d3d37 --- /dev/null +++ b/fixtures/camera_set_state_ok.json @@ -0,0 +1,4 @@ +{ + "status": "ok", + "time_server": 1582932411 +} \ No newline at end of file diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 96772267e..e953024c7 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -388,3 +388,45 @@ def test_CameraData_smokedetectorByName(cameraHomeData, name, home, home_id, exp ) else: assert cameraHomeData.smokedetectorByName(name, home, home_id)["id"] == expected + + +@pytest.mark.parametrize( + "home_id, camera_id, floodlight, monitoring, json_fixture, expected", + [ + (None, None, None, None, "camera_set_state_error.json", False), + ( + "91763b24c43d3e344f424e8b", + "12:34:56:00:f1:62", + None, + "on", + "camera_set_state_ok.json", + True, + ), + ], +) +def test_CameraData_set_state( + cameraHomeData, + requests_mock, + home_id, + camera_id, + floodlight, + monitoring, + json_fixture, + expected, +): + with open("fixtures/%s" % json_fixture) as f: + json_fixture = json.load(f) + requests_mock.post( + pyatmo.camera._SETSTATE_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + assert ( + cameraHomeData.set_state( + home_id=home_id, + camera_id=camera_id, + floodlight=floodlight, + monitoring=monitoring, + ) + == expected + ) From 7ecf337d010d2cea9250b208453da2f4cbb4462d Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 29 Feb 2020 15:01:19 +0100 Subject: [PATCH 197/333] Add more tests --- fixtures/camera_set_state_error.json | 6 + ...atherstation_data_unreachable_station.json | 271 ++++++++++++++++++ tests/test_pyatmo_camera.py | 17 +- 3 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 fixtures/camera_set_state_error.json create mode 100644 fixtures/weatherstation_data_unreachable_station.json diff --git a/fixtures/camera_set_state_error.json b/fixtures/camera_set_state_error.json new file mode 100644 index 000000000..43c25b230 --- /dev/null +++ b/fixtures/camera_set_state_error.json @@ -0,0 +1,6 @@ +{ + "error": { + "code": 21, + "message": "Invalid device_id, 12:34:56:00:f1:ff" + } +} \ No newline at end of file diff --git a/fixtures/weatherstation_data_unreachable_station.json b/fixtures/weatherstation_data_unreachable_station.json new file mode 100644 index 000000000..962b048a7 --- /dev/null +++ b/fixtures/weatherstation_data_unreachable_station.json @@ -0,0 +1,271 @@ +{ + "body": { + "devices": [ + { + "_id": "12:34:56:37:11:ca", + "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX", + "date_setup": 1544558432, + "last_setup": 1544558432, + "type": "NAMain", + "last_status_store": 1559413181, + "module_name": "NetatmoIndoor", + "firmware": 137, + "last_upgrade": 1544558433, + "wifi_status": 45, + "reachable": true, + "co2_calibrating": false, + "station_name": "MyStation", + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 664, + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [ + 52.516263, + 13.377726 + ] + }, + "dashboard_data": { + "time_utc": 1559413171, + "Temperature": 24.6, + "CO2": 749, + "Humidity": 36, + "Noise": 37, + "Pressure": 1017.3, + "AbsolutePressure": 939.7, + "min_temp": 23.4, + "max_temp": 25.6, + "date_min_temp": 1559371924, + "date_max_temp": 1559411964, + "temp_trend": "stable", + "pressure_trend": "down" + }, + "modules": [ + { + "_id": "12:34:56:36:fc:de", + "type": "NAModule1", + "module_name": "NetatmoOutdoor", + "data_type": [ + "Temperature", + "Humidity" + ], + "last_setup": 1544558433, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413157, + "Temperature": 28.6, + "Humidity": 24, + "min_temp": 16.9, + "max_temp": 30.3, + "date_min_temp": 1559365579, + "date_max_temp": 1559404698, + "temp_trend": "down" + }, + "firmware": 46, + "last_message": 1559413177, + "last_seen": 1559413157, + "rf_status": 65, + "battery_vp": 5738, + "battery_percent": 87 + }, + { + "_id": "12:34:56:07:bb:3e", + "type": "NAModule4", + "module_name": "Kitchen", + "data_type": [ + "Temperature", + "CO2", + "Humidity" + ], + "last_setup": 1548956696, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413125, + "Temperature": 28, + "CO2": 503, + "Humidity": 26, + "min_temp": 25, + "max_temp": 28, + "date_min_temp": 1559371577, + "date_max_temp": 1559412561, + "temp_trend": "up" + }, + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 73, + "battery_vp": 5687, + "battery_percent": 83 + }, + { + "_id": "12:34:56:07:bb:0e", + "type": "NAModule4", + "module_name": "Livingroom", + "data_type": [ + "Temperature", + "CO2", + "Humidity" + ], + "last_setup": 1548957209, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413093, + "Temperature": 26.4, + "CO2": 451, + "Humidity": 31, + "min_temp": 25.1, + "max_temp": 26.4, + "date_min_temp": 1559365290, + "date_max_temp": 1559413093, + "temp_trend": "stable" + }, + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413093, + "rf_status": 84, + "battery_vp": 5626, + "battery_percent": 79 + }, + { + "_id": "12:34:56:03:1b:e4", + "type": "NAModule2", + "module_name": "Garden", + "data_type": [ + "Wind" + ], + "last_setup": 1549193862, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "WindStrength": 4, + "WindAngle": 217, + "GustStrength": 9, + "GustAngle": 206, + "max_wind_str": 21, + "max_wind_angle": 217, + "date_max_wind_str": 1559386669 + }, + "firmware": 19, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 59, + "battery_vp": 5689, + "battery_percent": 85 + }, + { + "_id": "12:34:56:05:51:20", + "type": "NAModule3", + "module_name": "Yard", + "data_type": [ + "Rain" + ], + "last_setup": 1549194580, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "Rain": 0, + "sum_rain_24": 0, + "sum_rain_1": 0 + }, + "firmware": 8, + "last_message": 1559413177, + "last_seen": 1559413170, + "rf_status": 67, + "battery_vp": 5860, + "battery_percent": 93 + } + ] + }, + { + "_id": "12:34:56:00:aa:01", + "station_name": "MyRemoteStation", + "date_setup": 1499189962, + "last_setup": 1499189962, + "type": "NAMain", + "last_status_store": 1554506294, + "module_name": "Indoor", + "firmware": 132, + "last_upgrade": 1499189915, + "wifi_status": 46, + "reachable": false, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 6, + "city": "Harstad", + "country": "NO", + "timezone": "Europe/Oslo", + "location": [ + 59.903781, + 10.683728 + ] + }, + "modules": [ + { + "_id": "12:34:56:00:aa:02", + "type": "NAModule1", + "module_name": "Outdoor", + "data_type": [ + "Temperature", + "Humidity" + ], + "last_setup": 1499189902, + "battery_percent": 17, + "reachable": false, + "firmware": 44, + "last_message": 1536805739, + "last_seen": 1536696388, + "rf_status": 87, + "battery_vp": 4018, + "main_device": "12:34:56:00:aa:01" + }, + { + "_id": "12:34:56:00:aa:03", + "type": "NAModule2", + "module_name": "Wind Gauge", + "data_type": [ + "Wind" + ], + "last_setup": 1499190606, + "battery_percent": 3, + "reachable": false, + "firmware": 18, + "last_message": 1537259554, + "last_seen": 1537259554, + "rf_status": 74, + "battery_vp": 4013, + "main_device": "12:34:56:00:aa:01" + } + ] + } + ], + "user": { + "mail": "john@doe.com", + "administrative": { + "lang": "de-DE", + "reg_locale": "de-DE", + "country": "DE", + "unit": 0, + "windunit": 0, + "pressureunit": 0, + "feel_like_algo": 0 + } + } + }, + "status": "ok", + "time_exec": 0.91107702255249, + "time_server": 1559413602 +} \ No newline at end of file diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index e953024c7..1f0290feb 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -393,7 +393,14 @@ def test_CameraData_smokedetectorByName(cameraHomeData, name, home, home_id, exp @pytest.mark.parametrize( "home_id, camera_id, floodlight, monitoring, json_fixture, expected", [ - (None, None, None, None, "camera_set_state_error.json", False), + ( + "91763b24c43d3e344f424e8b", + "12:34:56:00:f1:ff", + "on", + None, + "camera_set_state_error.json", + False, + ), ( "91763b24c43d3e344f424e8b", "12:34:56:00:f1:62", @@ -402,6 +409,14 @@ def test_CameraData_smokedetectorByName(cameraHomeData, name, home, home_id, exp "camera_set_state_ok.json", True, ), + ( + "91763b24c43d3e344f424e8b", + "12:34:56:00:f1:62", + None, + "on", + "camera_set_state_error_already_on.json", + True, + ), ], ) def test_CameraData_set_state( From eed1b917452505b7b43ee920dd49b8343381f037 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 29 Feb 2020 15:15:34 +0100 Subject: [PATCH 198/333] Add test cases --- fixtures/camera_set_state_error_already_on.json | 17 +++++++++++++++++ .../camera_set_state_error_wrong_parameter.json | 6 ++++++ tests/test_pyatmo_camera.py | 8 ++++++++ 3 files changed, 31 insertions(+) create mode 100644 fixtures/camera_set_state_error_already_on.json create mode 100644 fixtures/camera_set_state_error_wrong_parameter.json diff --git a/fixtures/camera_set_state_error_already_on.json b/fixtures/camera_set_state_error_already_on.json new file mode 100644 index 000000000..1251647e5 --- /dev/null +++ b/fixtures/camera_set_state_error_already_on.json @@ -0,0 +1,17 @@ +{ + "status": "ok", + "time_server": 1582932399, + "body": { + "home": { + "id": "91763b24c43d3e344f424e8b" + }, + "errors": [ + { + "code": 23, + "message": "Already on", + "id": "12:34:56:00:f1:62", + "command": "command/changestatus" + } + ] + } +} \ No newline at end of file diff --git a/fixtures/camera_set_state_error_wrong_parameter.json b/fixtures/camera_set_state_error_wrong_parameter.json new file mode 100644 index 000000000..9a9c5121e --- /dev/null +++ b/fixtures/camera_set_state_error_wrong_parameter.json @@ -0,0 +1,6 @@ +{ + "error": { + "code": 21, + "message": "cannot set property floodlight for module 70:ee:50:1f:f1:62" + } +} \ No newline at end of file diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 1f0290feb..0334418ee 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -417,6 +417,14 @@ def test_CameraData_smokedetectorByName(cameraHomeData, name, home, home_id, exp "camera_set_state_error_already_on.json", True, ), + ( + "91763b24c43d3e344f424e8b", + "12:34:56:00:f1:62", + "on", + None, + "camera_set_state_error_wrong_parameter.json", + False, + ), ], ) def test_CameraData_set_state( From 396cbf5fb0a00f419d5b7ddddc7f9c75746a99e7 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 29 Feb 2020 15:19:57 +0100 Subject: [PATCH 199/333] Fix test fixture --- fixtures/camera_set_state_error_wrong_parameter.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fixtures/camera_set_state_error_wrong_parameter.json b/fixtures/camera_set_state_error_wrong_parameter.json index 9a9c5121e..9575dd2a4 100644 --- a/fixtures/camera_set_state_error_wrong_parameter.json +++ b/fixtures/camera_set_state_error_wrong_parameter.json @@ -1,6 +1,6 @@ { "error": { "code": 21, - "message": "cannot set property floodlight for module 70:ee:50:1f:f1:62" + "message": "cannot set property floodlight for module 12:34:56:00:f1:62" } } \ No newline at end of file From fd035af757a8af646609f3fbfd98c173bb83dc49 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 29 Feb 2020 18:17:04 +0100 Subject: [PATCH 200/333] Update tests --- fixtures/weatherstation_data_unreachable_station.json | 4 ++-- tests/test_pyatmo_camera.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/fixtures/weatherstation_data_unreachable_station.json b/fixtures/weatherstation_data_unreachable_station.json index 962b048a7..1642b86d7 100644 --- a/fixtures/weatherstation_data_unreachable_station.json +++ b/fixtures/weatherstation_data_unreachable_station.json @@ -209,8 +209,8 @@ "country": "NO", "timezone": "Europe/Oslo", "location": [ - 59.903781, - 10.683728 + 59.895000, + 10.620000 ] }, "modules": [ diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 0334418ee..ba30645bb 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -409,6 +409,14 @@ def test_CameraData_smokedetectorByName(cameraHomeData, name, home, home_id, exp "camera_set_state_ok.json", True, ), + ( + "91763b24c43d3e344f424e8b", + "12:34:56:00:f1:62", + "auto", + "on", + "camera_set_state_ok.json", + True, + ), ( "91763b24c43d3e344f424e8b", "12:34:56:00:f1:62", From fbc70d107cddaa4c218e9d7b6bdc879d5a02b134 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 29 Feb 2020 23:27:36 +0100 Subject: [PATCH 201/333] Make home id optional --- src/pyatmo/camera.py | 10 +++++++--- tests/test_pyatmo_camera.py | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 65bee6c56..a89e0a22c 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -81,6 +81,7 @@ def __init__(self, authData, size=15): self.events[e["camera_id"]][e["time"]] = e for c in item["cameras"]: self.cameras[homeId][c["id"]] = c + self.cameras[homeId][c["id"]]["home"] = homeId if c["type"] == "NACamera" and "modules" in c: for m in c["modules"]: self.modules[m["id"]] = m @@ -793,22 +794,25 @@ def module_opened(self, mid, cid, exclude=0): def set_state( self, - home_id: str, camera_id: str, + home_id: str = None, floodlight: str = None, monitoring: str = None, ) -> bool: - """Turn camera on/off. + """Turn camera (light) on/off. Arguments: - home_id {str} -- ID of a home camera_id {str} -- ID of a camera + home_id {str} -- ID of a home floodlight {str} -- Mode for floodlight (on/auto) monitoring {str} -- Mode for monitoring (on/off) Returns: Boolean -- Success of the request """ + if home_id is None: + home_id = self.get_camera(camera_id)["home"] + module = {"id": camera_id} if floodlight: diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index ba30645bb..6e0dbf9f8 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -409,6 +409,7 @@ def test_CameraData_smokedetectorByName(cameraHomeData, name, home, home_id, exp "camera_set_state_ok.json", True, ), + (None, "12:34:56:00:f1:62", None, "on", "camera_set_state_ok.json", True,), ( "91763b24c43d3e344f424e8b", "12:34:56:00:f1:62", From 37cd75ad739f5c69e8bc2bc1df85e0b90486538e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 2 Mar 2020 12:49:26 +0100 Subject: [PATCH 202/333] Fix token refresh issue --- src/pyatmo/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 266d64777..8f3189974 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -90,7 +90,7 @@ def __init__( def refresh_tokens(self) -> Dict[str, Union[str, int]]: """Refresh and return new tokens.""" - token = self._oauth.refresh_token(_AUTH_REQ) + token = self._oauth.refresh_token(_AUTH_REQ, **self.extra) if self.token_updater is not None: self.token_updater(token) From f3f2543ca312ed60b9601186e474902012658206 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sun, 8 Mar 2020 13:45:33 +0100 Subject: [PATCH 203/333] Add development branch to CI (#107) --- .github/workflows/pythonpackage.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 4290f7ed1..89186a62a 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -4,9 +4,11 @@ on: push: branches: - master + - development pull_request: branches: - master + - development jobs: black: From 806cc37c9629da3ee3c2dc0fad0d251a9447e1ab Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 8 Mar 2020 14:03:21 +0100 Subject: [PATCH 204/333] v3.3.0 * Add set state call for camera devices #106 * Add development branch to CI #107 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2efda642e..ed20bc8c4 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="pyatmo", - version="3.2.4", # Should be updated with new versions + version="3.3.0", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", packages=find_packages(exclude=["tests"], where="src"), From 9965e9a95a000d3058538d3c957c453c5a8d82e9 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 9 Mar 2020 13:50:41 +0100 Subject: [PATCH 205/333] Log exception --- src/pyatmo/auth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 8f3189974..10045680f 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -114,6 +114,8 @@ def post_request( resp = requests.post(url, data=params, timeout=timeout) except requests.exceptions.ChunkedEncodingError: LOG.debug("Encoding error when connecting to '%s'", url) + except requests.exceptions.ConnectTimeout: + LOG.debug("Connection to %s timed out.", url) else: def query(url, params, timeout, retries): From 5fba346afeb6bf1b72b3f0f8d153d6d266890a5c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 11 Mar 2020 08:50:40 +0100 Subject: [PATCH 206/333] Handle requests exceptions --- src/pyatmo/auth.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 10045680f..7cea5d1e1 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -115,7 +115,9 @@ def post_request( except requests.exceptions.ChunkedEncodingError: LOG.debug("Encoding error when connecting to '%s'", url) except requests.exceptions.ConnectTimeout: - LOG.debug("Connection to %s timed out.", url) + LOG.debug("Connection to %s timed out", url) + except requests.exceptions.ConnectionError: + LOG.debug("Remote end closed connection without response (%s)", url) else: def query(url, params, timeout, retries): From 91e33bdb575f0cef7814936e7202ea7d5eae4fcf Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 17 Mar 2020 08:40:05 +0100 Subject: [PATCH 207/333] Improve error handling --- src/pyatmo/auth.py | 13 +++++++------ src/pyatmo/helpers.py | 1 + tests/test_pyatmo.py | 7 +++++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 7cea5d1e1..180b29892 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -1,4 +1,5 @@ import logging +from json.decoder import JSONDecodeError from time import sleep from typing import Callable, Dict, Optional, Tuple, Union @@ -151,18 +152,18 @@ def query(url, params, timeout, retries): elif not resp.ok: LOG.debug("The Netatmo API returned %s", resp.status_code) LOG.debug("Netato API error: %s", resp.content) - if resp.status_code == 404: + try: raise ApiError( f"{resp.status_code} - " - f"{ERRORS[resp.status_code]} - " + f"{ERRORS.get(resp.status_code, '')} - " + f"{resp.json()['error']['message']} " + f"({resp.json()['error']['code']}) " f"when accessing '{url}'" ) - else: + except JSONDecodeError: raise ApiError( f"{resp.status_code} - " - f"{ERRORS[resp.status_code]} - " - f"{resp.json()['error']['message']} " - f"({resp.json()['error']['code']}) " + f"{ERRORS.get(resp.status_code, '')} - " f"when accessing '{url}'" ) diff --git a/src/pyatmo/helpers.py b/src/pyatmo/helpers.py index 5d7ab44f3..8a70720f4 100644 --- a/src/pyatmo/helpers.py +++ b/src/pyatmo/helpers.py @@ -14,6 +14,7 @@ 404: "Not found", 406: "Not Acceptable", 500: "Internal Server Error", + 502: "Bad Gateway", } diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index 18d03c5d4..8044699a3 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -2,8 +2,8 @@ import json import time -import pytest import oauthlib +import pytest import pyatmo @@ -56,10 +56,13 @@ def test_postRequest_binary(auth, requests_mock): assert resp == b"Success" -@pytest.mark.parametrize("test_input,expected", [(200, None), (404, None)]) +@pytest.mark.parametrize( + "test_input,expected", [(200, None), (404, None), (401, None)], +) def test_postRequest_fail(auth, requests_mock, test_input, expected): """Test failing requests against the Netatmo API.""" requests_mock.post(pyatmo.helpers._BASE_URL, status_code=test_input) + if test_input == 200: resp = auth.post_request(pyatmo.helpers._BASE_URL, None) assert resp is expected From ff0f48b8f2bdefa960310552af13b22db2803933 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 27 Mar 2020 11:25:38 +0100 Subject: [PATCH 208/333] Add light keyword and fix typo --- src/pyatmo/camera.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index a89e0a22c..247cb0d50 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -804,7 +804,7 @@ def set_state( Arguments: camera_id {str} -- ID of a camera home_id {str} -- ID of a home - floodlight {str} -- Mode for floodlight (on/auto) + floodlight {str} -- Mode for floodlight (on/off/auto) monitoring {str} -- Mode for monitoring (on/off) Returns: @@ -817,8 +817,8 @@ def set_state( if floodlight: param, val = "floodlight", floodlight.lower() - if val not in ["on", "auto"]: - LOG.error("Invalid value fÃŧr floodlight") + if val not in ["on", "off", "auto"]: + LOG.error("Invalid value for floodlight") else: module[param] = val From 081335cbbf36cde8be166d7942b7b3cce2d1162c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 4 May 2020 00:30:05 +0200 Subject: [PATCH 209/333] Catch more errors --- src/pyatmo/__main__.py | 18 +++++++----------- src/pyatmo/auth.py | 4 ++-- src/pyatmo/helpers.py | 1 + 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/pyatmo/__main__.py b/src/pyatmo/__main__.py index 8fa280130..bf24b9eb6 100644 --- a/src/pyatmo/__main__.py +++ b/src/pyatmo/__main__.py @@ -1,9 +1,9 @@ -from .auth import ClientAuth -from .camera import CameraData -from .exceptions import NoDevice -from .public_data import PublicData -from .thermostat import HomeData -from .weather_station import WeatherStationData +from pyatmo.auth import ALL_SCOPES, ClientAuth +from pyatmo.camera import CameraData +from pyatmo.exceptions import NoDevice +from pyatmo.public_data import PublicData +from pyatmo.thermostat import HomeData +from pyatmo.weather_station import WeatherStationData def main(): @@ -33,11 +33,7 @@ def main(): clientSecret=CLIENT_SECRET, username=USERNAME, password=PASSWORD, - scope=( - "read_station read_camera access_camera read_thermostat " - "write_thermostat read_presence access_presence read_homecoach " - "read_smokedetector" - ), + scope=" ".join(ALL_SCOPES), ) try: diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 180b29892..dc44fe406 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -1,5 +1,5 @@ import logging -from json.decoder import JSONDecodeError +from json import JSONDecodeError from time import sleep from typing import Callable, Dict, Optional, Tuple, Union @@ -173,7 +173,7 @@ def query(url, params, timeout, retries): if "application/json" in resp.headers.get("content-type") else resp.content ) - except TypeError: + except (TypeError, AttributeError): LOG.debug("Invalid response %s", resp) return None diff --git a/src/pyatmo/helpers.py b/src/pyatmo/helpers.py index 8a70720f4..39218a950 100644 --- a/src/pyatmo/helpers.py +++ b/src/pyatmo/helpers.py @@ -15,6 +15,7 @@ 406: "Not Acceptable", 500: "Internal Server Error", 502: "Bad Gateway", + 503: "Service Unavailable", } From 164b709eca4e29caeed0d6780cd5034c11e64203 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 4 May 2020 00:31:07 +0200 Subject: [PATCH 210/333] Don't expect values to be there --- src/pyatmo/thermostat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 38931f2be..8ae83f6a8 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -316,7 +316,7 @@ def measuredTemperature(self, rid=None): temperature = None room_data = self.roomById(rid=rid) if room_data: - temperature = room_data["therm_measured_temperature"] + temperature = room_data.get("therm_measured_temperature") return temperature def boilerStatus(self, rid=None): From 8dd94961683f76b042eac0da3ca1fd98cefd6881 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 4 May 2020 00:31:52 +0200 Subject: [PATCH 211/333] Sometimes no module names are returned in the JSON --- src/pyatmo/weather_station.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index c8424b0fc..9103fc507 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -86,7 +86,7 @@ def getModules(self, station=None, station_id=None): for m in s["modules"]: res[m["_id"]] = { "station_name": m.get("station_name", s["station_name"]), - "module_name": m["module_name"], + "module_name": m.get("module_name", "Module"), "id": m["_id"], } return res From c9d6550b15677b3220b3675835faf3e26b9b381c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 4 May 2020 00:55:48 +0200 Subject: [PATCH 212/333] Return type if station/module name not given --- fixtures/weatherstation_data_simple.json | 535 +++++++++++------------ src/pyatmo/weather_station.py | 6 +- tests/test_pyatmo_weatherstation.py | 4 +- 3 files changed, 253 insertions(+), 292 deletions(-) diff --git a/fixtures/weatherstation_data_simple.json b/fixtures/weatherstation_data_simple.json index 401633777..162bf834e 100644 --- a/fixtures/weatherstation_data_simple.json +++ b/fixtures/weatherstation_data_simple.json @@ -1,290 +1,251 @@ { - "body": { - "devices": [ - { - "_id": "12:34:56:37:11:ca", - "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX", - "date_setup": 1544558432, - "last_setup": 1544558432, - "type": "NAMain", - "last_status_store": 1559413181, - "module_name": "NetatmoIndoor", - "firmware": 137, - "last_upgrade": 1544558433, - "wifi_status": 45, - "reachable": true, - "co2_calibrating": false, - "station_name": "MyStation", - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure" - ], - "place": { - "altitude": 664, - "city": "Frankfurt", - "country": "DE", - "timezone": "Europe/Berlin", - "location": [ - 52.516263, - 13.377726 - ] - }, - "dashboard_data": { - "time_utc": 1559413171, - "Temperature": 24.6, - "CO2": 749, - "Humidity": 36, - "Noise": 37, - "Pressure": 1017.3, - "AbsolutePressure": 939.7, - "min_temp": 23.4, - "max_temp": 25.6, - "date_min_temp": 1559371924, - "date_max_temp": 1559411964, - "temp_trend": "stable", - "pressure_trend": "down" - }, - "modules": [ - { - "_id": "12:34:56:36:fc:de", - "type": "NAModule1", - "module_name": "NetatmoOutdoor", - "data_type": [ - "Temperature", - "Humidity" - ], - "last_setup": 1544558433, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413157, - "Temperature": 28.6, - "Humidity": 24, - "min_temp": 16.9, - "max_temp": 30.3, - "date_min_temp": 1559365579, - "date_max_temp": 1559404698, - "temp_trend": "down" - }, - "firmware": 46, - "last_message": 1559413177, - "last_seen": 1559413157, - "rf_status": 65, - "battery_vp": 5738, - "battery_percent": 87 - }, - { - "_id": "12:34:56:07:bb:3e", - "type": "NAModule4", - "module_name": "Kitchen", - "data_type": [ - "Temperature", - "CO2", - "Humidity" - ], - "last_setup": 1548956696, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413125, - "Temperature": 28, - "CO2": 503, - "Humidity": 26, - "min_temp": 25, - "max_temp": 28, - "date_min_temp": 1559371577, - "date_max_temp": 1559412561, - "temp_trend": "up" - }, - "firmware": 44, - "last_message": 1559413177, - "last_seen": 1559413177, - "rf_status": 73, - "battery_vp": 5687, - "battery_percent": 83 - }, - { - "_id": "12:34:56:07:bb:0e", - "type": "NAModule4", - "module_name": "Livingroom", - "data_type": [ - "Temperature", - "CO2", - "Humidity" - ], - "last_setup": 1548957209, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413093, - "Temperature": 26.4, - "CO2": 451, - "Humidity": 31, - "min_temp": 25.1, - "max_temp": 26.4, - "date_min_temp": 1559365290, - "date_max_temp": 1559413093, - "temp_trend": "stable" - }, - "firmware": 44, - "last_message": 1559413177, - "last_seen": 1559413093, - "rf_status": 84, - "battery_vp": 5626, - "battery_percent": 79 - }, - { - "_id": "12:34:56:03:1b:e4", - "type": "NAModule2", - "module_name": "Garden", - "data_type": [ - "Wind" - ], - "last_setup": 1549193862, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413170, - "WindStrength": 4, - "WindAngle": 217, - "GustStrength": 9, - "GustAngle": 206, - "max_wind_str": 21, - "max_wind_angle": 217, - "date_max_wind_str": 1559386669 - }, - "firmware": 19, - "last_message": 1559413177, - "last_seen": 1559413177, - "rf_status": 59, - "battery_vp": 5689, - "battery_percent": 85 - }, - { - "_id": "12:34:56:05:51:20", - "type": "NAModule3", - "module_name": "Yard", - "data_type": [ - "Rain" - ], - "last_setup": 1549194580, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413170, - "Rain": 0, - "sum_rain_24": 0, - "sum_rain_1": 0 - }, - "firmware": 8, - "last_message": 1559413177, - "last_seen": 1559413170, - "rf_status": 67, - "battery_vp": 5860, - "battery_percent": 93 - } - ] + "body": { + "devices": [ + { + "_id": "12:34:56:37:11:ca", + "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX", + "date_setup": 1544558432, + "last_setup": 1544558432, + "type": "NAMain", + "last_status_store": 1559413181, + "module_name": "NetatmoIndoor", + "firmware": 137, + "last_upgrade": 1544558433, + "wifi_status": 45, + "reachable": true, + "co2_calibrating": false, + "station_name": "MyStation", + "data_type": ["Temperature", "CO2", "Humidity", "Noise", "Pressure"], + "place": { + "altitude": 664, + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [52.516263, 13.377726] + }, + "dashboard_data": { + "time_utc": 1559413171, + "Temperature": 24.6, + "CO2": 749, + "Humidity": 36, + "Noise": 37, + "Pressure": 1017.3, + "AbsolutePressure": 939.7, + "min_temp": 23.4, + "max_temp": 25.6, + "date_min_temp": 1559371924, + "date_max_temp": 1559411964, + "temp_trend": "stable", + "pressure_trend": "down" + }, + "modules": [ + { + "_id": "12:34:56:36:fc:de", + "type": "NAModule1", + "module_name": "NetatmoOutdoor", + "data_type": ["Temperature", "Humidity"], + "last_setup": 1544558433, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413157, + "Temperature": 28.6, + "Humidity": 24, + "min_temp": 16.9, + "max_temp": 30.3, + "date_min_temp": 1559365579, + "date_max_temp": 1559404698, + "temp_trend": "down" }, - { - "_id": "12 :34: 56:36:fd:3c", - "station_name": "Valley Road", - "date_setup": 1545897146, - "last_setup": 1545897146, - "type": "NAMain", - "last_status_store": 1581835369, - "firmware": 137, - "last_upgrade": 1545897125, - "wifi_status": 53, - "reachable": true, - "co2_calibrating": false, - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure" - ], - "place": { - "altitude": 69, - "city": "Valley", - "country": "AU", - "timezone": "Australia/Hobart", - "location": [ - 148.444226, - -41.721282 - ] - }, - "read_only": true, - "dashboard_data": { - "time_utc": 1581835330, - "Temperature": 22.4, - "CO2": 471, - "Humidity": 46, - "Noise": 47, - "Pressure": 1011.5, - "AbsolutePressure": 1002.8, - "min_temp": 18.1, - "max_temp": 22.5, - "date_max_temp": 1581829891, - "date_min_temp": 1581794878, - "temp_trend": "stable", - "pressure_trend": "stable" - }, - "modules": [ - { - "_id": "12 :34: 56:36:e6:c0", - "type": "NAModule1", - "module_name": "Module", - "data_type": [ - "Temperature", - "Humidity" - ], - "last_setup": 1545897146, - "battery_percent": 22, - "reachable": false, - "firmware": 46, - "last_message": 1572497781, - "last_seen": 1572497742, - "rf_status": 88, - "battery_vp": 4118 - }, - { - "_id": "12:34:56:05:25:6e", - "type": "NAModule3", - "module_name": "Rain Gauge", - "data_type": [ - "Rain" - ], - "last_setup": 1553997427, - "battery_percent": 82, - "reachable": true, - "firmware": 8, - "last_message": 1581835362, - "last_seen": 1581835354, - "rf_status": 78, - "battery_vp": 5594, - "dashboard_data": { - "time_utc": 1581835329, - "Rain": 0, - "sum_rain_1": 0, - "sum_rain_24": 0 - } - } - ] - } - ], - "user": { - "mail": "john@doe.com", - "administrative": { - "lang": "de-DE", - "reg_locale": "de-DE", - "country": "DE", - "unit": 0, - "windunit": 0, - "pressureunit": 0, - "feel_like_algo": 0 + "firmware": 46, + "last_message": 1559413177, + "last_seen": 1559413157, + "rf_status": 65, + "battery_vp": 5738, + "battery_percent": 87 + }, + { + "_id": "12:34:56:07:bb:3e", + "type": "NAModule4", + "module_name": "Kitchen", + "data_type": ["Temperature", "CO2", "Humidity"], + "last_setup": 1548956696, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413125, + "Temperature": 28, + "CO2": 503, + "Humidity": 26, + "min_temp": 25, + "max_temp": 28, + "date_min_temp": 1559371577, + "date_max_temp": 1559412561, + "temp_trend": "up" + }, + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 73, + "battery_vp": 5687, + "battery_percent": 83 + }, + { + "_id": "12:34:56:07:bb:0e", + "type": "NAModule4", + "module_name": "Livingroom", + "data_type": ["Temperature", "CO2", "Humidity"], + "last_setup": 1548957209, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413093, + "Temperature": 26.4, + "CO2": 451, + "Humidity": 31, + "min_temp": 25.1, + "max_temp": 26.4, + "date_min_temp": 1559365290, + "date_max_temp": 1559413093, + "temp_trend": "stable" + }, + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413093, + "rf_status": 84, + "battery_vp": 5626, + "battery_percent": 79 + }, + { + "_id": "12:34:56:03:1b:e4", + "type": "NAModule2", + "module_name": "Garden", + "data_type": ["Wind"], + "last_setup": 1549193862, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "WindStrength": 4, + "WindAngle": 217, + "GustStrength": 9, + "GustAngle": 206, + "max_wind_str": 21, + "max_wind_angle": 217, + "date_max_wind_str": 1559386669 + }, + "firmware": 19, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 59, + "battery_vp": 5689, + "battery_percent": 85 + }, + { + "_id": "12:34:56:05:51:20", + "type": "NAModule3", + "module_name": "Yard", + "data_type": ["Rain"], + "last_setup": 1549194580, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "Rain": 0, + "sum_rain_24": 0, + "sum_rain_1": 0 + }, + "firmware": 8, + "last_message": 1559413177, + "last_seen": 1559413170, + "rf_status": 67, + "battery_vp": 5860, + "battery_percent": 93 + } + ] + }, + { + "_id": "12 :34: 56:36:fd:3c", + "station_name": "Valley Road", + "date_setup": 1545897146, + "last_setup": 1545897146, + "type": "NAMain", + "last_status_store": 1581835369, + "firmware": 137, + "last_upgrade": 1545897125, + "wifi_status": 53, + "reachable": true, + "co2_calibrating": false, + "data_type": ["Temperature", "CO2", "Humidity", "Noise", "Pressure"], + "place": { + "altitude": 69, + "city": "Valley", + "country": "AU", + "timezone": "Australia/Hobart", + "location": [148.444226, -41.721282] + }, + "read_only": true, + "dashboard_data": { + "time_utc": 1581835330, + "Temperature": 22.4, + "CO2": 471, + "Humidity": 46, + "Noise": 47, + "Pressure": 1011.5, + "AbsolutePressure": 1002.8, + "min_temp": 18.1, + "max_temp": 22.5, + "date_max_temp": 1581829891, + "date_min_temp": 1581794878, + "temp_trend": "stable", + "pressure_trend": "stable" + }, + "modules": [ + { + "_id": "12 :34: 56:36:e6:c0", + "type": "NAModule1", + "data_type": ["Temperature", "Humidity"], + "last_setup": 1545897146, + "battery_percent": 22, + "reachable": false, + "firmware": 46, + "last_message": 1572497781, + "last_seen": 1572497742, + "rf_status": 88, + "battery_vp": 4118 + }, + { + "_id": "12:34:56:05:25:6e", + "type": "NAModule3", + "module_name": "Rain Gauge", + "data_type": ["Rain"], + "last_setup": 1553997427, + "battery_percent": 82, + "reachable": true, + "firmware": 8, + "last_message": 1581835362, + "last_seen": 1581835354, + "rf_status": 78, + "battery_vp": 5594, + "dashboard_data": { + "time_utc": 1581835329, + "Rain": 0, + "sum_rain_1": 0, + "sum_rain_24": 0 } - } - }, - "status": "ok", - "time_exec": 0.91107702255249, - "time_server": 1559413602 -} \ No newline at end of file + } + ] + } + ], + "user": { + "mail": "john@doe.com", + "administrative": { + "lang": "de-DE", + "reg_locale": "de-DE", + "country": "DE", + "unit": 0, + "windunit": 0, + "pressureunit": 0, + "feel_like_algo": 0 + } + } + }, + "status": "ok", + "time_exec": 0.91107702255249, + "time_server": 1559413602 +} diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 9103fc507..89ed1c8dc 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -55,13 +55,13 @@ def modulesNamesList(self, station=None, station_id=None): elif station is not None: station_data = self.stationByName(station) if station_data is not None: - res.add(station_data["module_name"]) + res.add(station_data.get("module_name", station_data.get("type"))) for m in station_data["modules"]: - res.add(m["module_name"]) + res.add(m.get("module_name", m.get("type"))) else: res.update([m["module_name"] for m in self.modules.values()]) for s in self.stations.values(): - res.add(s.get("module_name", "Station")) + res.add(s.get("module_name", s.get("type"))) return list(res) def getModules(self, station=None, station_id=None): diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index b1d4852af..fe112d9d2 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -50,11 +50,10 @@ def test_WeatherStationData_no_data(auth, requests_mock): "Garden", "Kitchen", "Livingroom", - "Module", + "NAMain", "NetatmoIndoor", "NetatmoOutdoor", "Rain Gauge", - "Station", "Yard", ], ), @@ -69,6 +68,7 @@ def test_WeatherStationData_no_data(auth, requests_mock): "Yard", ], ), + ("Valley Road", ["NAMain", "NAModule1", "Rain Gauge"],), pytest.param( "NoValidStation", None, From 4ee5263340500412d55cfb5b694a0eb8bac52a38 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 6 May 2020 00:34:37 +0200 Subject: [PATCH 213/333] Add test data --- fixtures/weatherstation_data_simple.json | 721 +++++++++++++++-------- 1 file changed, 475 insertions(+), 246 deletions(-) diff --git a/fixtures/weatherstation_data_simple.json b/fixtures/weatherstation_data_simple.json index 162bf834e..27e83624e 100644 --- a/fixtures/weatherstation_data_simple.json +++ b/fixtures/weatherstation_data_simple.json @@ -1,251 +1,480 @@ { - "body": { - "devices": [ - { - "_id": "12:34:56:37:11:ca", - "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX", - "date_setup": 1544558432, - "last_setup": 1544558432, - "type": "NAMain", - "last_status_store": 1559413181, - "module_name": "NetatmoIndoor", - "firmware": 137, - "last_upgrade": 1544558433, - "wifi_status": 45, - "reachable": true, - "co2_calibrating": false, - "station_name": "MyStation", - "data_type": ["Temperature", "CO2", "Humidity", "Noise", "Pressure"], - "place": { - "altitude": 664, - "city": "Frankfurt", - "country": "DE", - "timezone": "Europe/Berlin", - "location": [52.516263, 13.377726] - }, - "dashboard_data": { - "time_utc": 1559413171, - "Temperature": 24.6, - "CO2": 749, - "Humidity": 36, - "Noise": 37, - "Pressure": 1017.3, - "AbsolutePressure": 939.7, - "min_temp": 23.4, - "max_temp": 25.6, - "date_min_temp": 1559371924, - "date_max_temp": 1559411964, - "temp_trend": "stable", - "pressure_trend": "down" - }, - "modules": [ - { - "_id": "12:34:56:36:fc:de", - "type": "NAModule1", - "module_name": "NetatmoOutdoor", - "data_type": ["Temperature", "Humidity"], - "last_setup": 1544558433, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413157, - "Temperature": 28.6, - "Humidity": 24, - "min_temp": 16.9, - "max_temp": 30.3, - "date_min_temp": 1559365579, - "date_max_temp": 1559404698, - "temp_trend": "down" + "body": { + "devices": [ + { + "_id": "12:34:56:37:11:ca", + "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX", + "date_setup": 1544558432, + "last_setup": 1544558432, + "type": "NAMain", + "last_status_store": 1559413181, + "module_name": "NetatmoIndoor", + "firmware": 137, + "last_upgrade": 1544558433, + "wifi_status": 45, + "reachable": true, + "co2_calibrating": false, + "station_name": "MyStation", + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 664, + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [ + 52.516263, + 13.377726 + ] + }, + "dashboard_data": { + "time_utc": 1559413171, + "Temperature": 24.6, + "CO2": 749, + "Humidity": 36, + "Noise": 37, + "Pressure": 1017.3, + "AbsolutePressure": 939.7, + "min_temp": 23.4, + "max_temp": 25.6, + "date_min_temp": 1559371924, + "date_max_temp": 1559411964, + "temp_trend": "stable", + "pressure_trend": "down" + }, + "modules": [ + { + "_id": "12:34:56:36:fc:de", + "type": "NAModule1", + "module_name": "NetatmoOutdoor", + "data_type": [ + "Temperature", + "Humidity" + ], + "last_setup": 1544558433, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413157, + "Temperature": 28.6, + "Humidity": 24, + "min_temp": 16.9, + "max_temp": 30.3, + "date_min_temp": 1559365579, + "date_max_temp": 1559404698, + "temp_trend": "down" + }, + "firmware": 46, + "last_message": 1559413177, + "last_seen": 1559413157, + "rf_status": 65, + "battery_vp": 5738, + "battery_percent": 87 + }, + { + "_id": "12:34:56:07:bb:3e", + "type": "NAModule4", + "module_name": "Kitchen", + "data_type": [ + "Temperature", + "CO2", + "Humidity" + ], + "last_setup": 1548956696, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413125, + "Temperature": 28, + "CO2": 503, + "Humidity": 26, + "min_temp": 25, + "max_temp": 28, + "date_min_temp": 1559371577, + "date_max_temp": 1559412561, + "temp_trend": "up" + }, + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 73, + "battery_vp": 5687, + "battery_percent": 83 + }, + { + "_id": "12:34:56:07:bb:0e", + "type": "NAModule4", + "module_name": "Livingroom", + "data_type": [ + "Temperature", + "CO2", + "Humidity" + ], + "last_setup": 1548957209, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413093, + "Temperature": 26.4, + "CO2": 451, + "Humidity": 31, + "min_temp": 25.1, + "max_temp": 26.4, + "date_min_temp": 1559365290, + "date_max_temp": 1559413093, + "temp_trend": "stable" + }, + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413093, + "rf_status": 84, + "battery_vp": 5626, + "battery_percent": 79 + }, + { + "_id": "12:34:56:03:1b:e4", + "type": "NAModule2", + "module_name": "Garden", + "data_type": [ + "Wind" + ], + "last_setup": 1549193862, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "WindStrength": 4, + "WindAngle": 217, + "GustStrength": 9, + "GustAngle": 206, + "max_wind_str": 21, + "max_wind_angle": 217, + "date_max_wind_str": 1559386669 + }, + "firmware": 19, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 59, + "battery_vp": 5689, + "battery_percent": 85 + }, + { + "_id": "12:34:56:05:51:20", + "type": "NAModule3", + "module_name": "Yard", + "data_type": [ + "Rain" + ], + "last_setup": 1549194580, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "Rain": 0, + "sum_rain_24": 0, + "sum_rain_1": 0 + }, + "firmware": 8, + "last_message": 1559413177, + "last_seen": 1559413170, + "rf_status": 67, + "battery_vp": 5860, + "battery_percent": 93 + } + ] }, - "firmware": 46, - "last_message": 1559413177, - "last_seen": 1559413157, - "rf_status": 65, - "battery_vp": 5738, - "battery_percent": 87 - }, - { - "_id": "12:34:56:07:bb:3e", - "type": "NAModule4", - "module_name": "Kitchen", - "data_type": ["Temperature", "CO2", "Humidity"], - "last_setup": 1548956696, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413125, - "Temperature": 28, - "CO2": 503, - "Humidity": 26, - "min_temp": 25, - "max_temp": 28, - "date_min_temp": 1559371577, - "date_max_temp": 1559412561, - "temp_trend": "up" + { + "_id": "12 :34: 56:36:fd:3c", + "station_name": "Valley Road", + "date_setup": 1545897146, + "last_setup": 1545897146, + "type": "NAMain", + "last_status_store": 1581835369, + "firmware": 137, + "last_upgrade": 1545897125, + "wifi_status": 53, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 69, + "city": "Valley", + "country": "AU", + "timezone": "Australia/Hobart", + "location": [ + 148.444226, + -41.721282 + ] + }, + "read_only": true, + "dashboard_data": { + "time_utc": 1581835330, + "Temperature": 22.4, + "CO2": 471, + "Humidity": 46, + "Noise": 47, + "Pressure": 1011.5, + "AbsolutePressure": 1002.8, + "min_temp": 18.1, + "max_temp": 22.5, + "date_max_temp": 1581829891, + "date_min_temp": 1581794878, + "temp_trend": "stable", + "pressure_trend": "stable" + }, + "modules": [ + { + "_id": "12 :34: 56:36:e6:c0", + "type": "NAModule1", + "module_name": "Module", + "data_type": [ + "Temperature", + "Humidity" + ], + "last_setup": 1545897146, + "battery_percent": 22, + "reachable": false, + "firmware": 46, + "last_message": 1572497781, + "last_seen": 1572497742, + "rf_status": 88, + "battery_vp": 4118 + }, + { + "_id": "12:34:56:05:25:6e", + "type": "NAModule3", + "module_name": "Rain Gauge", + "data_type": [ + "Rain" + ], + "last_setup": 1553997427, + "battery_percent": 82, + "reachable": true, + "firmware": 8, + "last_message": 1581835362, + "last_seen": 1581835354, + "rf_status": 78, + "battery_vp": 5594, + "dashboard_data": { + "time_utc": 1581835329, + "Rain": 0, + "sum_rain_1": 0, + "sum_rain_24": 0 + } + } + ] }, - "firmware": 44, - "last_message": 1559413177, - "last_seen": 1559413177, - "rf_status": 73, - "battery_vp": 5687, - "battery_percent": 83 - }, - { - "_id": "12:34:56:07:bb:0e", - "type": "NAModule4", - "module_name": "Livingroom", - "data_type": ["Temperature", "CO2", "Humidity"], - "last_setup": 1548957209, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413093, - "Temperature": 26.4, - "CO2": 451, - "Humidity": 31, - "min_temp": 25.1, - "max_temp": 26.4, - "date_min_temp": 1559365290, - "date_max_temp": 1559413093, - "temp_trend": "stable" + { + "_id": "12:34:56:32:a7:60", + "station_name": "Ateljen", + "date_setup": 1566714693, + "last_setup": 1566714693, + "type": "NAMain", + "last_status_store": 1588481079, + "module_name": "Indoor", + "firmware": 177, + "last_upgrade": 1566714694, + "wifi_status": 50, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 93, + "city": "Benareby", + "country": "SE", + "timezone": "Europe/Stockholm", + "location": [ + 12.1854725, + 57.637138 + ] + }, + "dashboard_data": { + "time_utc": 1588481073, + "Temperature": 18.2, + "CO2": 542, + "Humidity": 45, + "Noise": 45, + "Pressure": 1013, + "AbsolutePressure": 1001.9, + "min_temp": 18.2, + "max_temp": 19.5, + "date_max_temp": 1588456861, + "date_min_temp": 1588479561, + "temp_trend": "stable", + "pressure_trend": "up" + }, + "modules": [ + { + "_id": "12:34:56:32:db:06", + "type": "NAModule1", + "last_setup": 1587635819, + "data_type": [ + "Temperature", + "Humidity" + ], + "battery_percent": 100, + "reachable": false, + "firmware": 255, + "last_message": 0, + "last_seen": 0, + "rf_status": 255, + "battery_vp": 65535 + } + ] }, - "firmware": 44, - "last_message": 1559413177, - "last_seen": 1559413093, - "rf_status": 84, - "battery_vp": 5626, - "battery_percent": 79 - }, - { - "_id": "12:34:56:03:1b:e4", - "type": "NAModule2", - "module_name": "Garden", - "data_type": ["Wind"], - "last_setup": 1549193862, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413170, - "WindStrength": 4, - "WindAngle": 217, - "GustStrength": 9, - "GustAngle": 206, - "max_wind_str": 21, - "max_wind_angle": 217, - "date_max_wind_str": 1559386669 - }, - "firmware": 19, - "last_message": 1559413177, - "last_seen": 1559413177, - "rf_status": 59, - "battery_vp": 5689, - "battery_percent": 85 - }, - { - "_id": "12:34:56:05:51:20", - "type": "NAModule3", - "module_name": "Yard", - "data_type": ["Rain"], - "last_setup": 1549194580, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413170, - "Rain": 0, - "sum_rain_24": 0, - "sum_rain_1": 0 - }, - "firmware": 8, - "last_message": 1559413177, - "last_seen": 1559413170, - "rf_status": 67, - "battery_vp": 5860, - "battery_percent": 93 - } - ] - }, - { - "_id": "12 :34: 56:36:fd:3c", - "station_name": "Valley Road", - "date_setup": 1545897146, - "last_setup": 1545897146, - "type": "NAMain", - "last_status_store": 1581835369, - "firmware": 137, - "last_upgrade": 1545897125, - "wifi_status": 53, - "reachable": true, - "co2_calibrating": false, - "data_type": ["Temperature", "CO2", "Humidity", "Noise", "Pressure"], - "place": { - "altitude": 69, - "city": "Valley", - "country": "AU", - "timezone": "Australia/Hobart", - "location": [148.444226, -41.721282] - }, - "read_only": true, - "dashboard_data": { - "time_utc": 1581835330, - "Temperature": 22.4, - "CO2": 471, - "Humidity": 46, - "Noise": 47, - "Pressure": 1011.5, - "AbsolutePressure": 1002.8, - "min_temp": 18.1, - "max_temp": 22.5, - "date_max_temp": 1581829891, - "date_min_temp": 1581794878, - "temp_trend": "stable", - "pressure_trend": "stable" - }, - "modules": [ - { - "_id": "12 :34: 56:36:e6:c0", - "type": "NAModule1", - "data_type": ["Temperature", "Humidity"], - "last_setup": 1545897146, - "battery_percent": 22, - "reachable": false, - "firmware": 46, - "last_message": 1572497781, - "last_seen": 1572497742, - "rf_status": 88, - "battery_vp": 4118 - }, - { - "_id": "12:34:56:05:25:6e", - "type": "NAModule3", - "module_name": "Rain Gauge", - "data_type": ["Rain"], - "last_setup": 1553997427, - "battery_percent": 82, - "reachable": true, - "firmware": 8, - "last_message": 1581835362, - "last_seen": 1581835354, - "rf_status": 78, - "battery_vp": 5594, - "dashboard_data": { - "time_utc": 1581835329, - "Rain": 0, - "sum_rain_1": 0, - "sum_rain_24": 0 + { + "_id": "12:34:56:1c:68:2e", + "station_name": "Bol\u00e5s", + "date_setup": 1470935400, + "last_setup": 1470935400, + "type": "NAMain", + "last_status_store": 1588481399, + "module_name": "Inne - Nere", + "firmware": 177, + "last_upgrade": 1470935401, + "wifi_status": 13, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 93, + "city": "Gothenburg", + "country": "SE", + "timezone": "Europe/Stockholm", + "location": [ + 11.6136629, + 57.7006827 + ] + }, + "dashboard_data": { + "time_utc": 1588481387, + "Temperature": 20.8, + "CO2": 674, + "Humidity": 41, + "Noise": 34, + "Pressure": 1012.1, + "AbsolutePressure": 1001, + "min_temp": 20.8, + "max_temp": 22.2, + "date_max_temp": 1588456859, + "date_min_temp": 1588480176, + "temp_trend": "stable", + "pressure_trend": "up" + }, + "modules": [ + { + "_id": "12:34:56:02:b3:da", + "type": "NAModule3", + "module_name": "Regnm\u00e4tare", + "last_setup": 1470937706, + "data_type": [ + "Rain" + ], + "battery_percent": 81, + "reachable": true, + "firmware": 12, + "last_message": 1588481393, + "last_seen": 1588481386, + "rf_status": 67, + "battery_vp": 5582, + "dashboard_data": { + "time_utc": 1588481386, + "Rain": 0, + "sum_rain_1": 0, + "sum_rain_24": 0.1 + } + }, + { + "_id": "12:34:56:03:76:60", + "type": "NAModule4", + "module_name": "Inne - Uppe", + "last_setup": 1470938089, + "data_type": [ + "Temperature", + "CO2", + "Humidity" + ], + "battery_percent": 14, + "reachable": true, + "firmware": 50, + "last_message": 1588481393, + "last_seen": 1588481374, + "rf_status": 70, + "battery_vp": 4448, + "dashboard_data": { + "time_utc": 1588481374, + "Temperature": 19.6, + "CO2": 696, + "Humidity": 41, + "min_temp": 19.6, + "max_temp": 20.5, + "date_max_temp": 1588456817, + "date_min_temp": 1588481374, + "temp_trend": "stable" + } + }, + { + "_id": "12:34:56:32:db:06", + "type": "NAModule1", + "module_name": "Ute", + "last_setup": 1566326027, + "data_type": [ + "Temperature", + "Humidity" + ], + "battery_percent": 81, + "reachable": true, + "firmware": 50, + "last_message": 1588481393, + "last_seen": 1588481380, + "rf_status": 61, + "battery_vp": 5544, + "dashboard_data": { + "time_utc": 1588481380, + "Temperature": 6.4, + "Humidity": 91, + "min_temp": 3.6, + "max_temp": 6.4, + "date_max_temp": 1588481380, + "date_min_temp": 1588471383, + "temp_trend": "up" + } + } + ] + } + ], + "user": { + "mail": "john@doe.com", + "administrative": { + "lang": "de-DE", + "reg_locale": "de-DE", + "country": "DE", + "unit": 0, + "windunit": 0, + "pressureunit": 0, + "feel_like_algo": 0 } - } - ] - } - ], - "user": { - "mail": "john@doe.com", - "administrative": { - "lang": "de-DE", - "reg_locale": "de-DE", - "country": "DE", - "unit": 0, - "windunit": 0, - "pressureunit": 0, - "feel_like_algo": 0 - } - } - }, - "status": "ok", - "time_exec": 0.91107702255249, - "time_server": 1559413602 -} + } + }, + "status": "ok", + "time_exec": 0.91107702255249, + "time_server": 1559413602 +} \ No newline at end of file From c258955f1f97fd3c9d5a9536dd602209bbe8599b Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 6 May 2020 00:34:47 +0200 Subject: [PATCH 214/333] Update tests --- tests/test_pyatmo_weatherstation.py | 60 ++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index fe112d9d2..87ea4e178 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -48,12 +48,18 @@ def test_WeatherStationData_no_data(auth, requests_mock): None, [ "Garden", + "Indoor", + "Inne - Nere", + "Inne - Uppe", "Kitchen", "Livingroom", + "Module", "NAMain", "NetatmoIndoor", "NetatmoOutdoor", "Rain Gauge", + "Regnmätare", + "Ute", "Yard", ], ), @@ -68,7 +74,7 @@ def test_WeatherStationData_no_data(auth, requests_mock): "Yard", ], ), - ("Valley Road", ["NAMain", "NAModule1", "Rain Gauge"],), + ("Valley Road", ["Module", "NAMain", "Rain Gauge"],), pytest.param( "NoValidStation", None, @@ -88,11 +94,21 @@ def test_WeatherStationData_modulesNamesList(weatherStationData, station, expect ( None, { + "12:34:56:02:b3:da": { + "id": "12:34:56:02:b3:da", + "module_name": "Regnmätare", + "station_name": "BolÃĨs", + }, "12:34:56:03:1b:e4": { "id": "12:34:56:03:1b:e4", "module_name": "Garden", "station_name": "MyStation", }, + "12:34:56:03:76:60": { + "id": "12:34:56:03:76:60", + "module_name": "Inne - Uppe", + "station_name": "BolÃĨs", + }, "12:34:56:05:51:20": { "id": "12:34:56:05:51:20", "module_name": "Yard", @@ -118,6 +134,21 @@ def test_WeatherStationData_modulesNamesList(weatherStationData, station, expect "module_name": "NetatmoIndoor", "station_name": "MyStation", }, + "12:34:56:1c:68:2e": { + "id": "12:34:56:1c:68:2e", + "module_name": "Inne - Nere", + "station_name": "BolÃĨs", + }, + "12:34:56:32:a7:60": { + "id": "12:34:56:32:a7:60", + "module_name": "Indoor", + "station_name": "Ateljen", + }, + "12:34:56:32:db:06": { + "id": "12:34:56:32:db:06", + "module_name": "Ute", + "station_name": "BolÃĨs", + }, "12:34:56:36:e6:c0": { "id": "12:34:56:36:e6:c0", "module_name": "Module", @@ -125,7 +156,7 @@ def test_WeatherStationData_modulesNamesList(weatherStationData, station, expect }, "12:34:56:36:fd:3c": { "id": "12:34:56:36:fd:3c", - "module_name": "Station", + "module_name": "NAMain", "station_name": "Valley Road", }, "12:34:56:05:25:6e": { @@ -379,11 +410,16 @@ def test_WeatherStationData_monitoredConditions( False, [ "Garden", + "Indoor", + "Inne - Nere", + "Inne - Uppe", "Kitchen", "Livingroom", "NetatmoIndoor", "NetatmoOutdoor", "Rain Gauge", + "Regnmätare", + "Ute", "Yard", ], ), @@ -393,10 +429,15 @@ def test_WeatherStationData_monitoredConditions( False, [ "Garden", + "Indoor", + "Inne - Nere", + "Inne - Uppe", "Kitchen", "NetatmoIndoor", "NetatmoOutdoor", "Rain Gauge", + "Regnmätare", + "Ute", "Yard", ], ), @@ -405,10 +446,15 @@ def test_WeatherStationData_monitoredConditions( 798103, True, [ + "12:34:56:02:b3:da", "12:34:56:03:1b:e4", + "12:34:56:03:76:60", "12:34:56:05:25:6e", "12:34:56:05:51:20", "12:34:56:07:bb:3e", + "12:34:56:1c:68:2e", + "12:34:56:32:a7:60", + "12:34:56:32:db:06", "12:34:56:36:fc:de", "12:34:56:36:fd:3c", "12:34:56:37:11:ca", @@ -555,11 +601,16 @@ def test_WeatherStationData_checkNotUpdated( 798500, [ "Garden", + "Indoor", + "Inne - Nere", + "Inne - Uppe", "Kitchen", "Livingroom", "NetatmoIndoor", "NetatmoOutdoor", "Rain Gauge", + "Regnmätare", + "Ute", "Yard", ], ), @@ -622,11 +673,16 @@ def test_WeatherStationData_lastData_measurements(weatherStationData): None, None, [ + "12:34:56:02:b3:da", "12:34:56:03:1b:e4", + "12:34:56:03:76:60", "12:34:56:05:25:6e", "12:34:56:05:51:20", "12:34:56:07:bb:0e", "12:34:56:07:bb:3e", + "12:34:56:1c:68:2e", + "12:34:56:32:a7:60", + "12:34:56:32:db:06", "12:34:56:36:fc:de", "12:34:56:36:fd:3c", "12:34:56:37:11:ca", From 3042835812999daa3f0e73f5894d1b92d7626dc4 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 6 May 2020 00:35:20 +0200 Subject: [PATCH 215/333] Use device type if no module name available --- src/pyatmo/weather_station.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 89ed1c8dc..1fab6a07c 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -79,14 +79,14 @@ def getModules(self, station=None, station_id=None): for s in stations: res[s["_id"]] = { "station_name": s["station_name"], - "module_name": s.get("module_name", "Station"), + "module_name": s.get("module_name", s.get("type")), "id": s["_id"], } for m in s["modules"]: res[m["_id"]] = { "station_name": m.get("station_name", s["station_name"]), - "module_name": m.get("module_name", "Module"), + "module_name": m.get("module_name", m.get("type")), "id": m["_id"], } return res From 14f3972342479d12151d23d72b98ca8485d95706 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 6 May 2020 00:38:34 +0200 Subject: [PATCH 216/333] Use all scopes in test fixture --- tests/conftest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 81f66032b..56abaa407 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,9 +26,7 @@ def auth(requests_mock): clientSecret="CLIENT_SECRET", username="USERNAME", password="PASSWORD", - scope="read_station read_camera access_camera " - "read_thermostat write_thermostat " - "read_presence access_presence read_homecoach", + scope=" ".join(pyatmo.auth.ALL_SCOPES), ) return authorization From eb094690affd74c36064676de450ffebbf523b3f Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 6 May 2020 00:40:06 +0200 Subject: [PATCH 217/333] Fix location --- fixtures/weatherstation_data_simple.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fixtures/weatherstation_data_simple.json b/fixtures/weatherstation_data_simple.json index 27e83624e..6d5e1f78a 100644 --- a/fixtures/weatherstation_data_simple.json +++ b/fixtures/weatherstation_data_simple.json @@ -292,12 +292,12 @@ ], "place": { "altitude": 93, - "city": "Benareby", + "city": "Gothenburg", "country": "SE", "timezone": "Europe/Stockholm", "location": [ - 12.1854725, - 57.637138 + 11.6136629, + 57.7006827 ] }, "dashboard_data": { From 6cda13c5c7a7b0c0713a1640f8c1a0891c3b5537 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 7 May 2020 15:03:23 +0200 Subject: [PATCH 218/333] Reduce API calls --- src/pyatmo/thermostat.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 8ae83f6a8..52262e96b 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -172,9 +172,12 @@ def switch_home_schedule(self, schedule_id: str, home_id: str) -> bool: class HomeStatus: - def __init__(self, authData, home_id=None, home=None): + def __init__(self, authData, home_data=None, home_id=None, home=None): self.authData = authData - self.home_data = HomeData(authData) + if home_data is None: + self.home_data = HomeData(authData) + else: + self.home_data = home_data if home_id is not None: self.home_id = home_id From 6261b2193944ff7ff0787e51106eddc7f0346875 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 10 May 2020 21:22:24 +0200 Subject: [PATCH 219/333] Update dependencies and fix bleach vulnerabilities Signed-off-by: Hugo D. (jabesq) --- Pipfile | 3 +- Pipfile.lock | 363 +++++++++++++++++++++++++-------------------------- 2 files changed, 180 insertions(+), 186 deletions(-) diff --git a/Pipfile b/Pipfile index 727ec4553..2b99328d2 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,8 @@ black = "*" mypy = "*" flake8 = "*" pylint = "*" -docutils = "==0.14" +docutils = "*" +bleach = ">=3.1.4" pytest-mock = "*" pytest-cov = "*" requests-mock = "*" diff --git a/Pipfile.lock b/Pipfile.lock index e119feff9..ff9ba17aa 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "59d3e7d59518a230b6320797c1d5f9bbbcd6d7bc700c6475b345dde5935a1620" + "sha256": "9f8f3df23b192e91113bc20807b39657119a4226864d039f8db144ebd0af2723" }, "pipfile-spec": 6, "requires": { @@ -18,10 +18,10 @@ "default": { "certifi": { "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", + "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" ], - "version": "==2019.11.28" + "version": "==2020.4.5.1" }, "chardet": { "hashes": [ @@ -66,10 +66,10 @@ }, "urllib3": { "hashes": [ - "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", - "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" + "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", + "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" ], - "version": "==1.25.8" + "version": "==1.25.9" } }, "develop": { @@ -82,10 +82,10 @@ }, "astroid": { "hashes": [ - "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", - "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" + "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", + "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" ], - "version": "==2.3.3" + "version": "==2.4.1" }, "attrs": { "hashes": [ @@ -104,17 +104,18 @@ }, "bleach": { "hashes": [ - "sha256:44f69771e2ac81ff30d929d485b7f9919f3ad6d019b6b20c74f3b8687c3f70df", - "sha256:aa8b870d0f46965bac2c073a93444636b0e1ca74e9777e34f03dd494b8a59d48" + "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f", + "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b" ], - "version": "==3.1.1" + "index": "pypi", + "version": "==3.1.5" }, "certifi": { "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", + "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" ], - "version": "==2019.11.28" + "version": "==2020.4.5.1" }, "cfgv": { "hashes": [ @@ -132,46 +133,46 @@ }, "click": { "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], - "version": "==7.0" + "version": "==7.1.2" }, "coverage": { "hashes": [ - "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3", - "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c", - "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0", - "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477", - "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a", - "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf", - "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691", - "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73", - "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987", - "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894", - "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e", - "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef", - "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf", - "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68", - "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8", - "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954", - "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2", - "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40", - "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc", - "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc", - "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e", - "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d", - "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f", - "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc", - "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301", - "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea", - "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb", - "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af", - "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52", - "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37", - "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0" - ], - "version": "==5.0.3" + "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a", + "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355", + "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65", + "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7", + "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9", + "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1", + "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0", + "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55", + "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c", + "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6", + "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef", + "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019", + "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e", + "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0", + "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf", + "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24", + "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2", + "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c", + "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4", + "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0", + "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd", + "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04", + "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e", + "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730", + "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2", + "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768", + "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796", + "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7", + "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a", + "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489", + "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052" + ], + "version": "==5.1" }, "distlib": { "hashes": [ @@ -181,19 +182,11 @@ }, "docutils": { "hashes": [ - "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", - "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", - "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", + "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" ], "index": "pypi", - "version": "==0.14" - }, - "entrypoints": { - "hashes": [ - "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", - "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" - ], - "version": "==0.3" + "version": "==0.16" }, "filelock": { "hashes": [ @@ -204,11 +197,11 @@ }, "flake8": { "hashes": [ - "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", - "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" + "sha256:c09e7e4ea0d91fa36f7b8439ca158e592be56524f0b67c39ab0ea2b85ed8f9a4", + "sha256:f33c5320eaa459cdee6367016a4bf4ba2a9b81499ce56e6a32abbf0b8d3a2eb4" ], "index": "pypi", - "version": "==3.7.9" + "version": "==3.8.0a2" }, "freezegun": { "hashes": [ @@ -220,10 +213,10 @@ }, "identify": { "hashes": [ - "sha256:1222b648251bdcb8deb240b294f450fbf704c7984e08baa92507e4ea10b436d5", - "sha256:d824ebe21f38325c771c41b08a95a761db1982f1fc0eee37c6c97df3f1636b96" + "sha256:23c18d97bb50e05be1a54917ee45cc61d57cb96aedc06aabb2b02331edf0dbf0", + "sha256:88ed90632023e52a6495749c6732e61e08ec9f4f04e95484a5c37b9caf40283c" ], - "version": "==1.4.11" + "version": "==1.4.15" }, "idna": { "hashes": [ @@ -234,11 +227,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302", - "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b" + "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", + "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" ], "markers": "python_version < '3.8'", - "version": "==1.5.0" + "version": "==1.6.0" }, "isort": { "hashes": [ @@ -250,10 +243,10 @@ }, "keyring": { "hashes": [ - "sha256:1f393f7466314068961c7e1d508120c092bd71fa54e3d93b76180b526d4abc56", - "sha256:24ae23ab2d6adc59138339e56843e33ec7b0a6b2f06302662477085c6c0aca00" + "sha256:3401234209015144a5d75701e71cb47239e552b0882313e9f51e8976f9e27843", + "sha256:c53e0e5ccde3ad34284a40ce7976b5b3a3d6de70344c3f8ee44364cc340976ec" ], - "version": "==21.1.0" + "version": "==21.2.1" }, "lazy-object-proxy": { "hashes": [ @@ -297,23 +290,23 @@ }, "mypy": { "hashes": [ - "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a", - "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7", - "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2", - "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474", - "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0", - "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217", - "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749", - "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6", - "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf", - "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36", - "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b", - "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72", - "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1", - "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1" + "sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2", + "sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1", + "sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164", + "sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761", + "sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce", + "sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27", + "sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754", + "sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae", + "sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9", + "sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600", + "sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65", + "sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8", + "sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913", + "sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3" ], "index": "pypi", - "version": "==0.761" + "version": "==0.770" }, "mypy-extensions": { "hashes": [ @@ -330,17 +323,17 @@ }, "packaging": { "hashes": [ - "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73", - "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334" + "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", + "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" ], - "version": "==20.1" + "version": "==20.3" }, "pathspec": { "hashes": [ - "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", - "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96" + "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", + "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" ], - "version": "==0.7.0" + "version": "==0.8.0" }, "pkginfo": { "hashes": [ @@ -358,11 +351,11 @@ }, "pre-commit": { "hashes": [ - "sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6", - "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb" + "sha256:979b53dab1af35063a483bfe13b0fcbbf1a2cf8c46b60e0a9a8d08e8269647a1", + "sha256:f3e85e68c6d1cbe7828d3471896f1b192cfcf1c4d83bf26e26beeb5941855257" ], "index": "pypi", - "version": "==2.1.1" + "version": "==2.3.0" }, "py": { "hashes": [ @@ -373,47 +366,47 @@ }, "pycodestyle": { "hashes": [ - "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", - "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + "sha256:933bfe8d45355fbb35f9017d81fc51df8cb7ce58b82aca2568b870bf7bea1611", + "sha256:c1362bf675a7c0171fa5f795917c570c2e405a97e5dc473b51f3656075d73acc" ], - "version": "==2.5.0" + "version": "==2.6.0a1" }, "pyflakes": { "hashes": [ - "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", - "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", + "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], - "version": "==2.1.1" + "version": "==2.2.0" }, "pygments": { "hashes": [ - "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", - "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" + "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", + "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" ], - "version": "==2.5.2" + "version": "==2.6.1" }, "pylint": { "hashes": [ - "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", - "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" + "sha256:b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c", + "sha256:dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b" ], "index": "pypi", - "version": "==2.4.4" + "version": "==2.5.2" }, "pyparsing": { "hashes": [ - "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", - "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + "sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492", + "sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a" ], - "version": "==2.4.6" + "version": "==3.0.0a1" }, "pytest": { "hashes": [ - "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d", - "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6" + "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3", + "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698" ], "index": "pypi", - "version": "==5.3.5" + "version": "==5.4.2" }, "pytest-cov": { "hashes": [ @@ -425,11 +418,11 @@ }, "pytest-mock": { "hashes": [ - "sha256:b35eb281e93aafed138db25c8772b95d3756108b601947f89af503f8c629413f", - "sha256:cb67402d87d5f53c579263d37971a164743dc33c159dfb4fb4a86f37c5552307" + "sha256:997729451dfc36b851a9accf675488c7020beccda15e11c75632ee3d1b1ccd71", + "sha256:ce610831cedeff5331f4e2fc453a5dd65384303f680ab34bee2c6533855b431c" ], "index": "pypi", - "version": "==2.0.0" + "version": "==3.1.0" }, "python-dateutil": { "hashes": [ @@ -440,52 +433,52 @@ }, "pyyaml": { "hashes": [ - "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", - "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", - "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", - "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", - "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", - "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", - "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", - "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", - "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", - "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", - "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" ], - "version": "==5.3" + "version": "==5.3.1" }, "readme-renderer": { "hashes": [ - "sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f", - "sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d" + "sha256:cbe9db71defedd2428a1589cdc545f9bd98e59297449f69d721ef8f1cfced68d", + "sha256:cc4957a803106e820d05d14f71033092537a22daa4f406dfbdd61177e0936376" ], - "version": "==24.0" + "version": "==26.0" }, "regex": { "hashes": [ - "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431", - "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242", - "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1", - "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d", - "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045", - "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b", - "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400", - "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa", - "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0", - "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69", - "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74", - "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb", - "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26", - "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5", - "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2", - "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce", - "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab", - "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e", - "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70", - "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc", - "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0" - ], - "version": "==2020.2.20" + "sha256:021a0ae4d2baeeb60a3014805a2096cb329bd6d9f30669b7ad0da51a9cb73349", + "sha256:04d6e948ef34d3eac133bedc0098364a9e635a7914f050edb61272d2ddae3608", + "sha256:099568b372bda492be09c4f291b398475587d49937c659824f891182df728cdf", + "sha256:0ff50843535593ee93acab662663cb2f52af8e31c3f525f630f1dc6156247938", + "sha256:1b17bf37c2aefc4cac8436971fe6ee52542ae4225cfc7762017f7e97a63ca998", + "sha256:1e2255ae938a36e9bd7db3b93618796d90c07e5f64dd6a6750c55f51f8b76918", + "sha256:2bc6a17a7fa8afd33c02d51b6f417fc271538990297167f68a98cae1c9e5c945", + "sha256:3ab5e41c4ed7cd4fa426c50add2892eb0f04ae4e73162155cd668257d02259dd", + "sha256:3b059e2476b327b9794c792c855aa05531a3f3044737e455d283c7539bd7534d", + "sha256:4df91094ced6f53e71f695c909d9bad1cca8761d96fd9f23db12245b5521136e", + "sha256:5493a02c1882d2acaaf17be81a3b65408ff541c922bfd002535c5f148aa29f74", + "sha256:5b741ecc3ad3e463d2ba32dce512b412c319993c1bb3d999be49e6092a769fb2", + "sha256:652ab4836cd5531d64a34403c00ada4077bb91112e8bcdae933e2eae232cf4a8", + "sha256:669a8d46764a09f198f2e91fc0d5acdac8e6b620376757a04682846ae28879c4", + "sha256:73a10404867b835f1b8a64253e4621908f0d71150eb4e97ab2e7e441b53e9451", + "sha256:7ce4a213a96d6c25eeae2f7d60d4dad89ac2b8134ec3e69db9bc522e2c0f9388", + "sha256:8127ca2bf9539d6a64d03686fd9e789e8c194fc19af49b69b081f8c7e6ecb1bc", + "sha256:b5b5b2e95f761a88d4c93691716ce01dc55f288a153face1654f868a8034f494", + "sha256:b7c9f65524ff06bf70c945cd8d8d1fd90853e27ccf86026af2afb4d9a63d06b1", + "sha256:f7f2f4226db6acd1da228adf433c5c3792858474e49d80668ea82ac87cf74a03", + "sha256:fa09da4af4e5b15c0e8b4986a083f3fd159302ea115a6cc0649cd163435538b8" + ], + "version": "==2020.5.7" }, "requests": { "hashes": [ @@ -497,11 +490,11 @@ }, "requests-mock": { "hashes": [ - "sha256:510df890afe08d36eca5bb16b4aa6308a6f85e3159ad3013bac8b9de7bd5a010", - "sha256:88d3402dd8b3c69a9e4f9d3a73ad11b15920c6efd36bc27bf1f701cf4a8e4646" + "sha256:11215c6f4df72702aa357f205cf1e537cffd7392b3e787b58239bde5fb3db53b", + "sha256:e68f46844e4cee9d447150343c9ae875f99fa8037c6dcf5f15bf1fe9ab43d226" ], "index": "pypi", - "version": "==1.7.0" + "version": "==1.8.0" }, "requests-toolbelt": { "hashes": [ @@ -526,18 +519,18 @@ }, "tox": { "hashes": [ - "sha256:0cbe98369081fa16bd6f1163d3d0b2a62afa29d402ccfad2bd09fb2668be0956", - "sha256:676f1e3e7de245ad870f956436b84ea226210587d1f72c8dfb8cd5ac7b6f0e70" + "sha256:8d97bfaf70053ed3db56f57377288621f1bcc7621446d301927d18df93b1c4c3", + "sha256:af09c19478e8fc7ce7555b3d802ddf601b82684b874812c5857f774b8aee1b67" ], "index": "pypi", - "version": "==3.14.5" + "version": "==3.15.0" }, "tqdm": { "hashes": [ - "sha256:0d8b5afb66e23d80433102e9bd8b5c8b65d34c2a2255b2de58d97bd2ea8170fd", - "sha256:f35fb121bafa030bd94e74fcfd44f3c2830039a2ddef7fc87ef1c2d205237b24" + "sha256:4733c4a10d0f2a4d098d801464bdaf5240c7dadd2a7fde4ee93b0a0efd9fb25e", + "sha256:acdafb20f51637ca3954150d0405ff1a7edde0ff19e38fb99a80a66210d2a28f" ], - "version": "==4.43.0" + "version": "==4.46.0" }, "twine": { "hashes": [ @@ -576,32 +569,32 @@ }, "typing-extensions": { "hashes": [ - "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", - "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", - "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" + "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", + "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", + "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" ], - "version": "==3.7.4.1" + "version": "==3.7.4.2" }, "urllib3": { "hashes": [ - "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", - "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" + "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", + "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" ], - "version": "==1.25.8" + "version": "==1.25.9" }, "virtualenv": { "hashes": [ - "sha256:30ea90b21dabd11da5f509710ad3be2ae47d40ccbc717dfdd2efe4367c10f598", - "sha256:4a36a96d785428278edd389d9c36d763c5755844beb7509279194647b1ef47f1" + "sha256:b4c14d4d73a0c23db267095383c4276ef60e161f94fde0427f2f21a0132dde74", + "sha256:fd0e54dec8ac96c1c7c87daba85f0a59a7c37fe38748e154306ca21c73244637" ], - "version": "==20.0.7" + "version": "==20.0.20" }, "wcwidth": { "hashes": [ - "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", - "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" + "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", + "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" ], - "version": "==0.1.8" + "version": "==0.1.9" }, "webencodings": { "hashes": [ @@ -612,16 +605,16 @@ }, "wrapt": { "hashes": [ - "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" + "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" ], - "version": "==1.11.2" + "version": "==1.12.1" }, "zipp": { "hashes": [ - "sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2", - "sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a" + "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", + "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" ], - "version": "==3.0.0" + "version": "==3.1.0" } } } From 6c8f92c2c3d266ce9fa051e20b4a7752985dded2 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 10 May 2020 21:24:22 +0200 Subject: [PATCH 220/333] v3.3.1 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ed20bc8c4..e8e92c750 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="pyatmo", - version="3.3.0", # Should be updated with new versions + version="3.3.1", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", packages=find_packages(exclude=["tests"], where="src"), From 890884f39a0e659d908d78defa84de2abdbc0600 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Wed, 19 Feb 2020 22:56:43 +0100 Subject: [PATCH 221/333] Replace invalid name for method/class/functions... Signed-off-by: Hugo D. (jabesq) --- .pre-commit-config.yaml | 17 +- src/pyatmo/__init__.py | 4 +- src/pyatmo/__main__.py | 26 +- src/pyatmo/auth.py | 65 +-- src/pyatmo/camera.py | 587 +++++++++++----------------- src/pyatmo/helpers.py | 8 +- src/pyatmo/home_coach.py | 10 +- src/pyatmo/public_data.py | 128 +++--- src/pyatmo/thermostat.py | 232 +++++------ src/pyatmo/weather_station.py | 282 +++++++------ tests/conftest.py | 51 +-- tests/test_pyatmo.py | 51 +-- tests/test_pyatmo_camera.py | 212 +++++----- tests/test_pyatmo_homecoach.py | 19 +- tests/test_pyatmo_publicdata.py | 127 +++--- tests/test_pyatmo_thermostat.py | 249 ++++++------ tests/test_pyatmo_weatherstation.py | 109 +++--- tox.ini | 2 +- usage.md | 84 ++-- 19 files changed, 1093 insertions(+), 1170 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c3d5e36c..14804b137 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,11 +5,22 @@ exclude: ^(fixtures/) repos: - repo: local hooks: + - id: isort + name: isort + language: system + entry: pipenv run isort + types: [python] + exclude: tests/ - id: black name: black language: system entry: pipenv run black types: [python] + - id: pylint + name: pylint + language: system + entry: pipenv run pylint + types: [python] - id: flake8 name: flake8 language: system @@ -21,9 +32,3 @@ repos: entry: pipenv run mypy types: [python] exclude: tests/ - - id: isort - name: isort - language: system - entry: pipenv run isort - types: [python] - exclude: tests/ diff --git a/src/pyatmo/__init__.py b/src/pyatmo/__init__.py index fe17aa3b3..2c0ffb3d3 100644 --- a/src/pyatmo/__init__.py +++ b/src/pyatmo/__init__.py @@ -1,4 +1,4 @@ -from .auth import ClientAuth, NetatmOAuth2 +from .auth import ClientAuth, NetatmoOAuth2 from .camera import CameraData from .exceptions import ApiError, InvalidHome, InvalidRoom, NoDevice, NoSchedule from .home_coach import HomeCoachData @@ -15,7 +15,7 @@ "InvalidHome", "InvalidRoom", "ApiError", - "NetatmOAuth2", + "NetatmoOAuth2", "NoDevice", "NoSchedule", "PublicData", diff --git a/src/pyatmo/__main__.py b/src/pyatmo/__main__.py index bf24b9eb6..09026561e 100644 --- a/src/pyatmo/__main__.py +++ b/src/pyatmo/__main__.py @@ -1,3 +1,6 @@ +import os +import sys + from pyatmo.auth import ALL_SCOPES, ClientAuth from pyatmo.camera import CameraData from pyatmo.exceptions import NoDevice @@ -7,32 +10,29 @@ def main(): - import sys - try: - import os - if ( os.environ["CLIENT_ID"] and os.environ["CLIENT_SECRET"] and os.environ["USERNAME"] and os.environ["PASSWORD"] ): - CLIENT_ID = os.environ["CLIENT_ID"] - CLIENT_SECRET = os.environ["CLIENT_SECRET"] - USERNAME = os.environ["USERNAME"] - PASSWORD = os.environ["PASSWORD"] + client_id = os.environ["CLIENT_ID"] + client_secret = os.environ["CLIENT_SECRET"] + username = os.environ["USERNAME"] + password = os.environ["PASSWORD"] except KeyError: sys.stderr.write( - "No credentials passed to pyatmo.py (client_id, client_secret, username, password)\n" + "No credentials passed to pyatmo.py (client_id, client_secret, " + "username, password)\n" ) sys.exit(1) authorization = ClientAuth( - clientId=CLIENT_ID, - clientSecret=CLIENT_SECRET, - username=USERNAME, - password=PASSWORD, + client_id=client_id, + client_secret=client_secret, + username=username, + password=password, scope=" ".join(ALL_SCOPES), ) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index dc44fe406..8c52a71c3 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -7,16 +7,16 @@ from oauthlib.oauth2 import LegacyApplicationClient, TokenExpiredError from requests_oauthlib import OAuth2Session -from .exceptions import ApiError -from .helpers import _BASE_URL, ERRORS +from pyatmo.exceptions import ApiError +from pyatmo.helpers import BASE_URL, ERRORS LOG = logging.getLogger(__name__) # Common definitions -_AUTH_REQ = _BASE_URL + "oauth2/token" -_AUTH_URL = _BASE_URL + "oauth2/authorize" -_WEBHOOK_URL_ADD = _BASE_URL + "api/addwebhook" -_WEBHOOK_URL_DROP = _BASE_URL + "api/dropwebhook" +AUTH_REQ = BASE_URL + "oauth2/token" +AUTH_URL = BASE_URL + "oauth2/authorize" +WEBHOOK_URL_ADD = BASE_URL + "api/addwebhook" +WEBHOOK_URL_DROP = BASE_URL + "api/dropwebhook" # Possible scops @@ -35,7 +35,7 @@ ] -class NetatmOAuth2: +class NetatmoOAuth2: """ Handle authentication with OAuth2 @@ -60,8 +60,8 @@ class NetatmOAuth2: def __init__( self, - client_id: str = None, - client_secret: str = None, + client_id: str, + client_secret: str, redirect_uri: Optional[str] = None, token: Optional[Dict[str, str]] = None, token_updater: Optional[Callable[[str], None]] = None, @@ -76,10 +76,7 @@ def __init__( else: self.scope = " ".join(ALL_SCOPES) if not scope else scope - self.extra = { - "client_id": self.client_id, - "client_secret": self.client_secret, - } + self.extra = {"client_id": self.client_id, "client_secret": self.client_secret} self._oauth = OAuth2Session( client_id=self.client_id, @@ -91,7 +88,7 @@ def __init__( def refresh_tokens(self) -> Dict[str, Union[str, int]]: """Refresh and return new tokens.""" - token = self._oauth.refresh_token(_AUTH_REQ, **self.extra) + token = self._oauth.refresh_token(AUTH_REQ, **self.extra) if self.token_updater is not None: self.token_updater(token) @@ -102,6 +99,7 @@ def post_request( self, url: str, params: Optional[Dict[str, str]] = None, timeout: int = 5 ): """Wrapper for post requests.""" + resp = None if not params: params = {} @@ -175,10 +173,11 @@ def query(url, params, timeout, retries): ) except (TypeError, AttributeError): LOG.debug("Invalid response %s", resp) + return None def get_authorization_url(self, state: Optional[str] = None) -> Tuple[str, str]: - return self._oauth.authorization_url(_AUTH_URL, state) + return self._oauth.authorization_url(AUTH_URL, state) def request_token( self, authorization_response: Optional[str] = None, code: Optional[str] = None @@ -191,25 +190,25 @@ def request_token( :return: A token dict """ return self._oauth.fetch_token( - _AUTH_REQ, + AUTH_REQ, authorization_response=authorization_response, code=code, client_secret=self.client_secret, include_client_id=True, ) - def addwebhook(self, webhook_url): - postParams = {"url": webhook_url} - resp = self.post_request(_WEBHOOK_URL_ADD, postParams) + def addwebhook(self, webhook_url: str) -> None: + post_params = {"url": webhook_url} + resp = self.post_request(WEBHOOK_URL_ADD, post_params) LOG.debug("addwebhook: %s", resp) - def dropwebhook(self): - postParams = {"app_types": "app_security"} - resp = self.post_request(_WEBHOOK_URL_DROP, postParams) + def dropwebhook(self) -> None: + post_params = {"app_types": "app_security"} + resp = self.post_request(WEBHOOK_URL_DROP, post_params) LOG.debug("dropwebhook: %s", resp) -class ClientAuth(NetatmOAuth2): +class ClientAuth(NetatmoOAuth2): """ Request authentication and keep access token available through token method. Renew it automatically if necessary Args: @@ -232,17 +231,23 @@ class ClientAuth(NetatmOAuth2): """ def __init__( - self, clientId, clientSecret, username, password, scope="read_station" + self, + client_id: str, + client_secret: str, + username: str, + password: str, + scope="read_station", ): - self._clientId = clientId - self._clientSecret = clientSecret + # pylint: disable=super-init-not-called + self._client_id = client_id + self._client_secret = client_secret - self._oauth = OAuth2Session(client=LegacyApplicationClient(client_id=clientId)) + self._oauth = OAuth2Session(client=LegacyApplicationClient(client_id=client_id)) self._oauth.fetch_token( - token_url=_AUTH_REQ, + token_url=AUTH_REQ, username=username, password=password, - client_id=clientId, - client_secret=clientSecret, + client_id=client_id, + client_secret=client_secret, scope=scope, ) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 247cb0d50..1116a1341 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -1,18 +1,19 @@ import imghdr import time -from typing import Dict, Tuple +from pprint import pformat +from typing import Dict, Optional, Tuple from requests.exceptions import ReadTimeout -from .exceptions import ApiError, InvalidHome, NoDevice -from .helpers import _BASE_URL, LOG +from pyatmo.exceptions import ApiError, InvalidHome, NoDevice +from pyatmo.helpers import BASE_URL, LOG -_GETHOMEDATA_REQ = _BASE_URL + "api/gethomedata" -_GETCAMERAPICTURE_REQ = _BASE_URL + "api/getcamerapicture" -_GETEVENTSUNTIL_REQ = _BASE_URL + "api/geteventsuntil" -_SETPERSONSAWAY_REQ = _BASE_URL + "api/setpersonsaway" -_SETPERSONSHOME_REQ = _BASE_URL + "api/setpersonshome" -_SETSTATE_REQ = _BASE_URL + "api/setstate" +_GETHOMEDATA_REQ = BASE_URL + "api/gethomedata" +_GETCAMERAPICTURE_REQ = BASE_URL + "api/getcamerapicture" +_GETEVENTSUNTIL_REQ = BASE_URL + "api/geteventsuntil" +_SETPERSONSAWAY_REQ = BASE_URL + "api/setpersonsaway" +_SETPERSONSHOME_REQ = BASE_URL + "api/setpersonshome" +_SETSTATE_REQ = BASE_URL + "api/setstate" class CameraData: @@ -20,84 +21,97 @@ class CameraData: List the Netatmo camera informations (Homes, cameras, smoke detectors, modules, events, persons) Args: - authData (ClientAuth): + auth_data (ClientAuth): Authentication information with a working access Token """ - def __init__(self, authData, size=15): - self.authData = authData - postParams = {"size": size} - resp = self.authData.post_request(url=_GETHOMEDATA_REQ, params=postParams) + def __init__(self, auth_data, size=15): + self.auth_data = auth_data + post_params = {"size": size} + resp = self.auth_data.post_request(url=_GETHOMEDATA_REQ, params=post_params) if resp is None or "body" not in resp: raise NoDevice("No device data returned by Netatmo server") - self.rawData = resp["body"].get("homes") - if not self.rawData: + + self.raw_data = resp["body"].get("homes") + if not self.raw_data: raise NoDevice("No device data available") - self.homes = {d["id"]: d for d in self.rawData} + + self.homes = {d["id"]: d for d in self.raw_data} if not self.homes: raise NoDevice("No device available") + self.persons = {} self.events = {} self.outdoor_events = {} self.cameras = {} self.smokedetectors = {} self.modules = {} - self.lastEvent = {} - self.outdoor_lastEvent = {} + self.last_event = {} + self.outdoor_last_event = {} self.types = {} self.default_home = None self.default_home_id = None self.default_camera = None self.default_smokedetector = None - for item in self.rawData: - homeId = item.get("id") - nameHome = item.get("name") - if not nameHome: - nameHome = "Unknown" - self.homes[homeId]["name"] = nameHome - if not homeId: + for item in self.raw_data: + home_id = item.get("id") + home_name = item.get("name") + + if not home_name: + home_name = "Unknown" + self.homes[home_id]["name"] = home_name + if not home_id: LOG.error('No key ["id"] in %s', item.keys()) continue - if homeId not in self.cameras: - self.cameras[homeId] = {} - if homeId not in self.smokedetectors: - self.smokedetectors[homeId] = {} - if homeId not in self.types: - self.types[homeId] = {} - for p in item["persons"]: - self.persons[p["id"]] = p + + if home_id not in self.cameras: + self.cameras[home_id] = {} + if home_id not in self.smokedetectors: + self.smokedetectors[home_id] = {} + if home_id not in self.types: + self.types[home_id] = {} + + for person in item["persons"]: + self.persons[person["id"]] = person + if "events" in item: if not self.default_home and not self.default_home_id: self.default_home = item["name"] self.default_home_id = item["id"] - for e in item["events"]: - if e["type"] == "outdoor": - if e["camera_id"] not in self.outdoor_events: - self.outdoor_events[e["camera_id"]] = {} - self.outdoor_events[e["camera_id"]][e["time"]] = e - elif e["type"] != "outdoor": - if e["camera_id"] not in self.events: - self.events[e["camera_id"]] = {} - self.events[e["camera_id"]][e["time"]] = e - for c in item["cameras"]: - self.cameras[homeId][c["id"]] = c - self.cameras[homeId][c["id"]]["home"] = homeId - if c["type"] == "NACamera" and "modules" in c: - for m in c["modules"]: - self.modules[m["id"]] = m - self.modules[m["id"]]["cam_id"] = c["id"] - for s in item["smokedetectors"]: - self.smokedetectors[homeId][s["id"]] = s - for t in item["cameras"]: - self.types[homeId][t["type"]] = t - for t in item["smokedetectors"]: - self.types[homeId][t["type"]] = t + + for event in item["events"]: + if event["type"] == "outdoor": + if event["camera_id"] not in self.outdoor_events: + self.outdoor_events[event["camera_id"]] = {} + self.outdoor_events[event["camera_id"]][event["time"]] = event + elif event["type"] != "outdoor": + if event["camera_id"] not in self.events: + self.events[event["camera_id"]] = {} + self.events[event["camera_id"]][event["time"]] = event + + for camera in item["cameras"]: + self.cameras[home_id][camera["id"]] = camera + self.cameras[home_id][camera["id"]]["home"] = home_id + if camera["type"] == "NACamera" and "modules" in camera: + for module in camera["modules"]: + self.modules[module["id"]] = module + self.modules[module["id"]]["cam_id"] = camera["id"] + + for smoke_detector in item["smokedetectors"]: + self.smokedetectors[home_id][smoke_detector["id"]] = smoke_detector + + for camera_type in item["cameras"]: + self.types[home_id][camera_type["type"]] = camera_type + + for camera_type in item["smokedetectors"]: + self.types[home_id][camera_type["type"]] = camera_type + for camera in self.events: - self.lastEvent[camera] = self.events[camera][ + self.last_event[camera] = self.events[camera][ sorted(self.events[camera])[-1] ] for camera in self.outdoor_events: - self.outdoor_lastEvent[camera] = self.outdoor_events[camera][ + self.outdoor_last_event[camera] = self.outdoor_events[camera][ sorted(self.outdoor_events[camera])[-1] ] if self.modules != {}: @@ -107,18 +121,19 @@ def __init__(self, authData, size=15): if self.default_home is not None and self.cameras[self.default_home_id]: self.default_camera = list(self.cameras[self.default_home_id].values())[0] - def homeById(self, hid): + def home_by_id(self, hid): return None if hid not in self.homes else self.homes[hid] - def homeByName(self, home=None): + def home_by_name(self, home=None): if not home: - return self.homeByName(self.default_home) + return self.home_by_name(self.default_home) + for key, value in self.homes.items(): if value["name"] == home: return self.homes[key] raise InvalidHome() - def getHomeName(self, home_id=None): + def get_home_name(self, home_id=None): if home_id is None: home_id = self.default_home_id for key, value in self.homes.items(): @@ -126,7 +141,7 @@ def getHomeName(self, home_id=None): return self.homes[key]["name"] raise InvalidHome("Invalid Home ID %s" % home_id) - def gethomeId(self, home=None): + def get_home_id(self, home=None): if not home: home = self.default_home for key, value in self.homes.items(): @@ -134,57 +149,62 @@ def gethomeId(self, home=None): return self.homes[key]["id"] raise InvalidHome("Invalid Home %s" % home) - def cameraById(self, cid): + def camera_by_id(self, cid): """Get camera data by ID.""" return self.get_camera(cid) - def get_camera(self, cid: str) -> Dict[str, str]: + def get_camera(self, cid: str) -> Optional[Dict[str, str]]: """Get camera data.""" for home_id, _ in self.cameras.items(): if cid in self.cameras[home_id]: return self.cameras[home_id][cid] return None - def cameraByName(self, camera=None, home=None, home_id=None): + def camera_by_name(self, camera=None, home=None, home_id=None): """Get camera data by name.""" if home_id is None: if home is None: hid = self.default_home_id + else: try: - hid = self.homeByName(home)["id"] + hid = self.home_by_name(home)["id"] except InvalidHome: LOG.debug("Invalid home %s", home) return None else: hid = home_id + if camera is None and home is None and home_id is None: return self.default_camera + if not (home_id or home) and camera: for h_id, cam_ids in self.cameras.items(): for cam_id in cam_ids: if self.cameras[h_id][cam_id]["name"] == camera: return self.cameras[h_id][cam_id] + elif hid and camera: if hid not in self.cameras: return None + for cam_id in self.cameras[hid]: if self.cameras[hid][cam_id]["name"] == camera: return self.cameras[hid][cam_id] else: return list(self.cameras[hid].values())[0] - def moduleById(self, mid): + def module_by_id(self, mid): return None if mid not in self.modules else self.modules[mid] - def moduleByName(self, module=None, camera=None, home=None): + def module_by_name(self, module=None, camera=None, home=None): if not module: if self.default_module: - return self.moduleByName(self.default_module) + return self.module_by_name(self.default_module) return None cam = None if camera or home: - cam = self.cameraByName(camera, home) + cam = self.camera_by_name(camera, home) if not cam: return None for key, value in self.modules.items(): @@ -194,33 +214,36 @@ def moduleByName(self, module=None, camera=None, home=None): return self.modules[key] return None - def smokedetectorById(self, sid): - for home, sd in self.smokedetectors.items(): + def smokedetector_by_id(self, sid): + for home, _ in self.smokedetectors.items(): if sid in self.smokedetectors[home]: return self.smokedetectors[home][sid] return None - def smokedetectorByName(self, smokedetector=None, home=None, home_id=None): + def smokedetector_by_name(self, smokedetector=None, home=None, home_id=None): if home_id is None: if home is None: hid = self.default_home_id else: try: - hid = self.homeByName(home)["id"] + hid = self.home_by_name(home)["id"] except InvalidHome: LOG.debug("Invalid home %s", home) return None else: hid = home_id + if smokedetector is None and home is None and home_id is None: return self.default_smokedetector - elif not (home_id or home) and smokedetector: + + if not (home_id or home) and smokedetector: for h_id, cam_ids in self.smokedetectors.items(): for cam_id in cam_ids: if self.smokedetectors[h_id][cam_id]["name"] == smokedetector: return self.smokedetectors[h_id][cam_id] + elif hid and smokedetector: - hid = self.homeByName(home)["id"] + hid = self.home_by_name(home)["id"] if hid not in self.smokedetectors: return None for cam_id in self.smokedetectors[hid]: @@ -229,7 +252,7 @@ def smokedetectorByName(self, smokedetector=None, home=None, home_id=None): else: return list(self.smokedetectors[hid].values())[0] - def cameraType(self, camera=None, home=None, cid=None, home_id=None): + def camera_type(self, camera=None, home=None, cid=None, home_id=None): """ Return the type of a given camera. """ @@ -237,26 +260,27 @@ def cameraType(self, camera=None, home=None, cid=None, home_id=None): if cid: camera_data = self.get_camera(cid) else: - camera_data = self.cameraByName(camera=camera, home=home, home_id=home_id) + camera_data = self.camera_by_name(camera=camera, home=home, home_id=home_id) if camera_data: cameratype = camera_data["type"] return cameratype - def cameraUrls( - self, camera: str = None, home: str = None, cid: str = None, home_id: str = None - ) -> Tuple[str, str]: + def camera_urls_by_name( + self, camera: str = None, home: str = None, home_id: str = None + ) -> Optional[Tuple[Optional[str], Optional[str]]]: """ Return the vpn_url and the local_url (if available) of a given camera in order to access its live feed (old interface) """ if home_id: - cid = self.cameraByName(camera=camera, home_id=home_id)["id"] + cid = self.camera_by_name(camera=camera, home_id=home_id)["id"] else: - cid = self.cameraByName(camera=camera, home=home)["id"] - return self.camera_urls(cid=cid) + cid = self.camera_by_name(camera=camera, home=home)["id"] + + return self.camera_urls(cid=cid) if cid is not None else None - def camera_urls(self, cid: str) -> Tuple[str, str]: + def camera_urls(self, cid: str) -> Tuple[Optional[str], Optional[str]]: """ Return the vpn_url and the local_url (if available) of a given camera in order to access its live feed @@ -270,11 +294,11 @@ def camera_urls(self, cid: str) -> Tuple[str, str]: vpn_url = camera_data.get("vpn_url") if camera_data.get("is_local"): - def check_url(url): + def check_url(url: Optional[str]) -> Optional[str]: if url is None: return None try: - resp = self.authData.post_request(url=f"{url}/command/ping") + resp = self.auth_data.post_request(url=f"{url}/command/ping") except (ApiError, ReadTimeout): LOG.debug("Timeout validation the camera url %s", url) return None @@ -283,13 +307,18 @@ def check_url(url): temp_local_url = check_url(vpn_url) local_url = check_url(temp_local_url) + return vpn_url, local_url - def get_light_state(self, cid: str) -> str: + def get_light_state(self, cid: str) -> Optional[str]: """Return the current mode of the floodlight of a presence camera.""" - return self.get_camera(cid).get("light_mode_status") + camera_data = self.get_camera(cid) + if camera_data is None: + raise ValueError("Invalid Camera ID") + + return camera_data.get("light_mode_status") - def personsAtHome(self, home=None, home_id=None): + def persons_at_home_by_name(self, home=None): """ Return the list of known persons who are currently at home (old interface) @@ -297,48 +326,31 @@ def personsAtHome(self, home=None, home_id=None): if not home: home_id = self.default_home_id else: - home_id = self.homeByName(home)["id"] + home_id = self.home_by_name(home)["id"] return self.persons_at_home(home_id) def persons_at_home(self, home_id=None): """ Return the list of known persons who are currently at home """ - home_data = self.homeById(home_id) - atHome = [] - for p in home_data["persons"]: + home_data = self.home_by_id(home_id) + at_home = [] + for person in home_data["persons"]: # Only check known personshome - if "pseudo" in p: - if not p["out_of_sight"]: - atHome.append(p["pseudo"]) - return atHome - - def setPersonsHome(self, person_ids, home_id): - """ - Mark persons as home. - (old interface) - """ - return self.set_persons_home(person_ids, home_id) + if "pseudo" in person: + if not person["out_of_sight"]: + at_home.append(person["pseudo"]) + return at_home def set_persons_home(self, person_ids, home_id): """ Mark persons as home. """ - postParams = { - "home_id": home_id, - "person_ids[]": person_ids, - } - resp = self.authData.post_request(url=_SETPERSONSHOME_REQ, params=postParams) + post_params = {"home_id": home_id, "person_ids[]": person_ids} + resp = self.auth_data.post_request(url=_SETPERSONSHOME_REQ, params=post_params) return resp - def setPersonsAway(self, person_id, home_id): - """ - Mark a person as away or set the whole home to being empty. - (old interface) - """ - return self.set_persons_away(person_id, home_id) - - def set_persons_away(self, person_id, home_id): + def set_person_away(self, person_id, home_id): """Mark a person as away or set the whole home to being empty. Arguments: @@ -348,20 +360,10 @@ def set_persons_away(self, person_id, home_id): Returns: [type] -- [description] """ - postParams = { - "home_id": home_id, - "person_id": person_id, - } - resp = self.authData.post_request(url=_SETPERSONSAWAY_REQ, params=postParams) + post_params = {"home_id": home_id, "person_id": person_id} + resp = self.auth_data.post_request(url=_SETPERSONSAWAY_REQ, params=post_params) return resp - def getPersonId(self, name): - """ - Retrieve a persons id - (old interface) - """ - return self.get_person_id(name) - def get_person_id(self, name): """Retrieve the ID of a person @@ -376,31 +378,30 @@ def get_person_id(self, name): return pid return None - def getCameraPicture(self, image_id, key): + def get_camera_picture(self, image_id, key): """ Download a specific image (of an event or user face) from the camera """ - postParams = { - "image_id": image_id, - "key": key, - } - resp = self.authData.post_request(url=_GETCAMERAPICTURE_REQ, params=postParams) + post_params = {"image_id": image_id, "key": key} + resp = self.auth_data.post_request( + url=_GETCAMERAPICTURE_REQ, params=post_params + ) image_type = imghdr.what("NONE.FILE", resp) return resp, image_type - def getProfileImage(self, name): + def get_profile_image(self, name): """ Retrieve the face of a given person """ - for p in self.persons: - if "pseudo" in self.persons[p]: - if name == self.persons[p]["pseudo"]: - image_id = self.persons[p]["face"]["id"] - key = self.persons[p]["face"]["key"] - return self.getCameraPicture(image_id, key) + for person in self.persons: + if "pseudo" in self.persons[person]: + if name == self.persons[person]["pseudo"]: + image_id = self.persons[person]["face"]["id"] + key = self.persons[person]["face"]["key"] + return self.get_camera_picture(image_id, key) return None, None - def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): + def update_event(self, event=None, home=None, devicetype=None, home_id=None): """ Update the list of events """ @@ -408,7 +409,7 @@ def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): try: if not home: home = self.default_home - home_id = self.gethomeId(home) + home_id = self.get_home_id(home) except InvalidHome: LOG.debug("Invalid Home %s", home) return None @@ -418,82 +419,69 @@ def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): if not event: # If no event is provided we need to retrieve the oldest of # the last event seen by each camera - listEvent = {} - for cam_id in self.lastEvent: - listEvent[self.lastEvent[cam_id]["time"]] = self.lastEvent[cam_id] - event = listEvent[sorted(listEvent)[0]] + event_set = {} + for cam_id in self.last_event: + event_set[self.last_event[cam_id]["time"]] = self.last_event[cam_id] + event = event_set[sorted(event_set)[0]] if devicetype == "NOC": # for the Presence camera if not event: # If no event is provided we need to retrieve the oldest of # the last event seen by each camera - listEvent = {} - for cam_id in self.outdoor_lastEvent: - listEvent[ - self.outdoor_lastEvent[cam_id]["time"] - ] = self.outdoor_lastEvent[cam_id] - event = listEvent[sorted(listEvent)[0]] + event_set = {} + for cam_id in self.outdoor_last_event: + event_set[ + self.outdoor_last_event[cam_id]["time"] + ] = self.outdoor_last_event[cam_id] + event = event_set[sorted(event_set)[0]] if devicetype == "NSD": # for the smoke detector if not event: # If no event is provided we need to retrieve the oldest of # the last event by each smoke detector - listEvent = {} - for sid in self.outdoor_lastEvent: - listEvent[ - self.outdoor_lastEvent[sid]["time"] - ] = self.outdoor_lastEvent[sid] - event = listEvent[sorted(listEvent)[0]] - - postParams = { - "home_id": home_id, - "event_id": event["id"], - } - - eventList = [] + event_set = {} + for sid in self.outdoor_last_event: + event_set[ + self.outdoor_last_event[sid]["time"] + ] = self.outdoor_last_event[sid] + event = event_set[sorted(event_set)[0]] + + post_params = {"home_id": home_id, "event_id": event["id"]} + event_list = [] + resp = None try: - resp = self.authData.post_request( - url=_GETEVENTSUNTIL_REQ, params=postParams + resp = self.auth_data.post_request( + url=_GETEVENTSUNTIL_REQ, params=post_params ) - eventList = resp["body"]["events_list"] + event_list = resp["body"]["events_list"] except ApiError: pass except KeyError: - LOG.debug("eventList response: %s", resp) - LOG.debug("eventList body: %s", resp["body"]) - - for e in eventList: - if e["type"] == "outdoor": - if e["camera_id"] not in self.outdoor_events: - self.outdoor_events[e["camera_id"]] = {} - self.outdoor_events[e["camera_id"]][e["time"]] = e - elif e["type"] != "outdoor": - if e["camera_id"] not in self.events: - self.events[e["camera_id"]] = {} - self.events[e["camera_id"]][e["time"]] = e + LOG.debug("eventList response: %s", pformat(resp)) + LOG.debug("eventList body: %s", resp.get("body", "UNKNOWN")) + + for _event in event_list: + if _event["type"] == "outdoor": + if _event["camera_id"] not in self.outdoor_events: + self.outdoor_events[event["camera_id"]] = {} + self.outdoor_events[_event["camera_id"]][_event["time"]] = _event + + elif _event["type"] != "outdoor": + if _event["camera_id"] not in self.events: + self.events[_event["camera_id"]] = {} + self.events[_event["camera_id"]][event["time"]] = _event + for camera in self.events: - self.lastEvent[camera] = self.events[camera][ + self.last_event[camera] = self.events[camera][ sorted(self.events[camera])[-1] ] for camera in self.outdoor_events: - self.outdoor_lastEvent[camera] = self.outdoor_events[camera][ + self.outdoor_last_event[camera] = self.outdoor_events[camera][ sorted(self.outdoor_events[camera])[-1] ] - def personSeenByCamera(self, name, home=None, camera=None, exclude=0): - """ - Evaluate if a specific person has been seen - (old interface) - """ - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("personSeenByCamera: Camera name or home is unknown") - return False - return self.person_seen_by_camera(name=name, cid=cid, exclude=exclude) - def person_seen_by_camera(self, name, cid, exclude=0): """ Evaluate if a specific person has been seen @@ -510,42 +498,30 @@ def person_seen_by_camera(self, name, cid, exclude=0): if "pseudo" in self.persons[person_id]: if self.persons[person_id]["pseudo"] == name: return True - elif self.lastEvent[cid]["type"] == "person": - person_id = self.lastEvent[cid]["person_id"] + + elif self.last_event[cid]["type"] == "person": + person_id = self.last_event[cid]["person_id"] if "pseudo" in self.persons[person_id]: if self.persons[person_id]["pseudo"] == name: return True return False - def _knownPersons(self): + def _known_persons_dict(self): known_persons = {} - for p_id, p in self.persons.items(): - if "pseudo" in p: - known_persons[p_id] = p + for person_id, person in self.persons.items(): + if "pseudo" in person: + known_persons[person_id] = person return known_persons - def knownPersons(self): - return {pid: p["pseudo"] for pid, p in self._knownPersons().items()} + def known_persons(self): + return {pid: p["pseudo"] for pid, p in self._known_persons_dict().items()} - def knownPersonsNames(self): + def known_persons_names(self): names = [] - for _, p in self._knownPersons().items(): - names.append(p["pseudo"]) + for _, person in self._known_persons_dict().items(): + names.append(person["pseudo"]) return names - def someoneKnownSeen(self, home=None, camera=None, exclude=0, cid=None): - """ - Evaluate if someone known has been seen - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("someoneKnownSeen: Camera name or home is unknown") - return False - return self.someone_known_seen(cid=cid, exclude=exclude) - def someone_known_seen(self, cid, exclude=0): """ Evaluate if someone known has been seen @@ -557,27 +533,17 @@ def someone_known_seen(self, cid, exclude=0): if time_ev < limit: return False if self.events[cid][time_ev]["type"] == "person": - if self.events[cid][time_ev]["person_id"] in self._knownPersons(): + if ( + self.events[cid][time_ev]["person_id"] + in self._known_persons_dict() + ): return True # Check in the last event if someone known has been seen - elif self.lastEvent[cid]["type"] == "person": - if self.lastEvent[cid]["person_id"] in self._knownPersons(): + elif self.last_event[cid]["type"] == "person": + if self.last_event[cid]["person_id"] in self._known_persons_dict(): return True return False - def someoneUnknownSeen(self, home=None, camera=None, exclude=0, cid=None): - """ - Evaluate if someone unknown has been seen - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("someoneUnknownSeen: Camera name or home is unknown") - return False - return self.someone_unknown_seen(cid=cid, exclude=exclude) - def someone_unknown_seen(self, cid, exclude=0): if exclude: limit = time.time() - exclude @@ -588,28 +554,15 @@ def someone_unknown_seen(self, cid, exclude=0): if self.events[cid][time_ev]["type"] == "person": if ( self.events[cid][time_ev]["person_id"] - not in self._knownPersons() + not in self._known_persons_dict() ): return True # Check in the last event is someone known has been seen - elif self.lastEvent[cid]["type"] == "person": - if self.lastEvent[cid]["person_id"] not in self._knownPersons(): + elif self.last_event[cid]["type"] == "person": + if self.last_event[cid]["person_id"] not in self._known_persons_dict(): return True return False - def motionDetected(self, home=None, camera=None, exclude=0, cid=None): - """ - Evaluate if movement has been detected - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("motionDetected: Camera name or home is unknown") - return False - return self.motion_detected(cid=cid, exclude=exclude) - def motion_detected(self, cid, exclude=0): """ Evaluate if movement has been detected @@ -622,122 +575,60 @@ def motion_detected(self, cid, exclude=0): return False if self.events[cid][time_ev]["type"] == "movement": return True - elif self.lastEvent[cid]["type"] == "movement": + elif self.last_event[cid]["type"] == "movement": return True return False - def outdoormotionDetected(self, home=None, camera=None, offset=0, cid=None): - """ - Evaluate if outdoor movement has been detected - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("outdoormotionDetected: Camera name or home is unknown") - return False - return self.outdoor_motion_detected(cid=cid, offset=0) - def outdoor_motion_detected(self, cid, offset=0): """ Evaluate if outdoor movement has been detected """ - if cid in self.lastEvent: - if self.lastEvent[cid]["type"] == "movement": - if self.lastEvent[cid][ + if cid in self.last_event: + if self.last_event[cid]["type"] == "movement": + if self.last_event[cid][ "video_status" - ] == "recording" and self.lastEvent[cid]["time"] + offset > int( + ] == "recording" and self.last_event[cid]["time"] + offset > int( time.time() ): return True return False - def humanDetected(self, home=None, camera=None, offset=0, cid=None): - """ - Evaluate if a human has been detected - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("personSeenByCamera: Camera name or home is unknown") - return False - return self.human_detected(cid=cid, offset=0) - def human_detected(self, cid, offset=0): """ Evaluate if a human has been detected """ - if self.outdoor_lastEvent[cid]["video_status"] == "recording": - for e in self.outdoor_lastEvent[cid]["event_list"]: - if e["type"] == "human" and e["time"] + offset > int(time.time()): + if self.outdoor_last_event[cid]["video_status"] == "recording": + for event in self.outdoor_last_event[cid]["event_list"]: + if event["type"] == "human" and event["time"] + offset > int( + time.time() + ): return True return False - def animalDetected(self, home=None, camera=None, offset=0, cid=None): - """ - Evaluate if an animal has been detected - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("animalDetected: Camera name or home is unknown") - return False - return self.animal_detected(cid=cid, offset=0) - def animal_detected(self, cid, offset=0): """ Evaluate if an animal has been detected """ - if self.outdoor_lastEvent[cid]["video_status"] == "recording": - for e in self.outdoor_lastEvent[cid]["event_list"]: - if e["type"] == "animal" and e["time"] + offset > int(time.time()): + if self.outdoor_last_event[cid]["video_status"] == "recording": + for event in self.outdoor_last_event[cid]["event_list"]: + if event["type"] == "animal" and event["time"] + offset > int( + time.time() + ): return True return False - def carDetected(self, home=None, camera=None, offset=0, cid=None): - """ - Evaluate if a car has been detected - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("carDetected: Camera name or home is unknown") - return False - return self.car_detected(cid=cid, offset=offset) - def car_detected(self, cid, offset=0): """ Evaluate if a car has been detected """ - if self.outdoor_lastEvent[cid]["video_status"] == "recording": - for e in self.outdoor_lastEvent[cid]["event_list"]: - if e["type"] == "vehicle" and e["time"] + offset > int(time.time()): + if self.outdoor_last_event[cid]["video_status"] == "recording": + for event in self.outdoor_last_event[cid]["event_list"]: + if event["type"] == "vehicle" and event["time"] + offset > int( + time.time() + ): return True return False - def moduleMotionDetected(self, module=None, home=None, camera=None, exclude=0): - """ - Evaluate if movement has been detected - (old interface) - """ - try: - mod = self.moduleByName(module, camera=camera, home=home) - mid = mod["id"] - cid = mod["cam_id"] - except TypeError: - LOG.error( - "moduleMotionDetected: Module name or Camera name or home is unknown" - ) - return False - return self.module_motion_detected(mid=mid, cid=cid, exclude=exclude) - def module_motion_detected(self, mid, cid, exclude=0): """ Evaluate if movement has been detected @@ -754,26 +645,26 @@ def module_motion_detected(self, mid, cid, exclude=0): ) and self.events[cid][time_ev]["module_id"] == mid: return True elif ( - self.lastEvent[cid]["type"] == "tag_big_move" - or self.lastEvent[cid]["type"] == "tag_small_move" - ) and self.lastEvent[cid]["module_id"] == mid: + self.last_event[cid]["type"] == "tag_big_move" + or self.last_event[cid]["type"] == "tag_small_move" + ) and self.last_event[cid]["module_id"] == mid: return True return False - def moduleOpened(self, module=None, home=None, camera=None, exclude=0): + def is_module_opened(self, module=None, home=None, camera=None, exclude=0): """ Evaluate if module status is open """ try: - mod = self.moduleByName(module, camera=camera, home=home) + mod = self.module_by_name(module, camera=camera, home=home) mid = mod["id"] cid = mod["cam_id"] except TypeError: - LOG.error("moduleOpened: Camera name, or home, or module is unknown") + LOG.error("is_module_opened: Camera name, or home, or module is unknown") return False - return self.module_opened(mid=mid, cid=cid, exclude=exclude) + return self._module_opened(mid=mid, cid=cid, exclude=exclude) - def module_opened(self, mid, cid, exclude=0): + def _module_opened(self, mid, cid, exclude=0): if exclude: limit = time.time() - exclude array_time_event = sorted(self.events[cid], reverse=True) @@ -786,8 +677,8 @@ def module_opened(self, mid, cid, exclude=0): ): return True elif ( - self.lastEvent[cid]["type"] == "tag_open" - and self.lastEvent[cid]["module_id"] == mid + self.last_event[cid]["type"] == "tag_open" + and self.last_event[cid]["module_id"] == mid ): return True return False @@ -829,12 +720,12 @@ def set_state( else: module[param] = val - postParams = { + post_params = { "json": {"home": {"id": home_id, "modules": [module]}}, } try: - resp = self.authData.post_request(url=_SETSTATE_REQ, params=postParams) + resp = self.auth_data.post_request(url=_SETSTATE_REQ, params=post_params) except ApiError as err_msg: LOG.error("%s", err_msg) return False diff --git a/src/pyatmo/helpers.py b/src/pyatmo/helpers.py index 39218a950..cf0de055e 100644 --- a/src/pyatmo/helpers.py +++ b/src/pyatmo/helpers.py @@ -5,7 +5,7 @@ LOG = logging.getLogger(__name__) -_BASE_URL = "https://api.netatmo.com/" +BASE_URL = "https://api.netatmo.com/" ERRORS = { 400: "Bad request", @@ -19,15 +19,15 @@ } -def toTimeString(value): +def to_time_string(value: str) -> str: return datetime.utcfromtimestamp(int(value)).isoformat(sep="_") -def toEpoch(value): +def to_epoch(value: str) -> int: return timegm(time.strptime(value + "GMT", "%Y-%m-%d_%H:%M:%S%Z")) -def todayStamps(): +def today_stamps(): today = timegm(time.strptime(time.strftime("%Y-%m-%d") + "GMT", "%Y-%m-%d%Z")) return today, today + 3600 * 24 diff --git a/src/pyatmo/home_coach.py b/src/pyatmo/home_coach.py index 21e11180d..fcf1e85cc 100644 --- a/src/pyatmo/home_coach.py +++ b/src/pyatmo/home_coach.py @@ -1,15 +1,15 @@ -from .helpers import _BASE_URL +from .helpers import BASE_URL from .weather_station import WeatherStationData -_GETHOMECOACHDATA_REQ = _BASE_URL + "api/gethomecoachsdata" +_GETHOMECOACHDATA_REQ = BASE_URL + "api/gethomecoachsdata" class HomeCoachData(WeatherStationData): """ List the Home Couch devices (stations and modules) Args: - authData (ClientAuth): Authentication information with a working access Token + auth_data (ClientAuth): Authentication information with a working access Token """ - def __init__(self, authData): - super(HomeCoachData, self).__init__(authData, urlReq=_GETHOMECOACHDATA_REQ) + def __init__(self, auth_data): + super(HomeCoachData, self).__init__(auth_data, url_req=_GETHOMECOACHDATA_REQ) diff --git a/src/pyatmo/public_data.py b/src/pyatmo/public_data.py index 18e655b82..aac06f9bf 100644 --- a/src/pyatmo/public_data.py +++ b/src/pyatmo/public_data.py @@ -1,7 +1,7 @@ -from .exceptions import NoDevice -from .helpers import _BASE_URL, toTimeString +from pyatmo.exceptions import NoDevice +from pyatmo.helpers import BASE_URL, to_time_string -_GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata" +_GETPUBLIC_DATA = BASE_URL + "api/getpublicdata" _LON_NE = 6.221652 _LAT_NE = 46.610870 _LON_SW = 6.217828 @@ -33,8 +33,8 @@ def __init__( required_data_type=None, # comma-separated list from above _STATION or _ACCESSORY values filtering=False, ): - self.authData = authData - postParams = { + self.auth_data = authData + post_params = { "lat_ne": LAT_NE, "lon_ne": LON_NE, "lat_sw": LAT_SW, @@ -43,106 +43,90 @@ def __init__( } if required_data_type: - postParams["required_data"] = required_data_type + post_params["required_data"] = required_data_type - resp = self.authData.post_request(url=_GETPUBLIC_DATA, params=postParams) + resp = self.auth_data.post_request(url=_GETPUBLIC_DATA, params=post_params) try: self.raw_data = resp["body"] except (KeyError, TypeError): raise NoDevice("No public weather data returned by Netatmo server") self.status = resp["status"] - self.time_exec = toTimeString(resp["time_exec"]) - self.time_server = toTimeString(resp["time_server"]) + self.time_exec = to_time_string(resp["time_exec"]) + self.time_server = to_time_string(resp["time_server"]) - def CountStationInArea(self): + def count_station_in_area(self): return len(self.raw_data) - # Backwards compatibility for < 1.2 - def getLive(self): - return self.getLatestRain() + def get_latest_rain(self): + return self.get_accessory_measures(_ACCESSORY_RAIN_LIVE_TYPE) - def getLatestRain(self): - return self.getAccessoryMeasures(_ACCESSORY_RAIN_LIVE_TYPE) + def get_average_rain(self): + return average_measure(self.get_latest_rain()) - def getAverageRain(self): - return averageMeasure(self.getLatestRain()) + def get_60min_rain(self): + return self.get_accessory_measures(_ACCESSORY_RAIN_60MIN_TYPE) - # Backwards compatibility for < 1.2 - def get60min(self): - return self.get60minRain() + def get_average_60min_rain(self): + return average_measure(self.get_60min_rain()) - def get60minRain(self): - return self.getAccessoryMeasures(_ACCESSORY_RAIN_60MIN_TYPE) + def get_24h_rain(self): + return self.get_accessory_measures(_ACCESSORY_RAIN_24H_TYPE) - def getAverage60minRain(self): - return averageMeasure(self.get60minRain()) + def get_average_24h_rain(self): + return average_measure(self.get_24h_rain()) - # Backwards compatibility for < 1.2 - def get24h(self): - return self.get24hRain() + def get_latest_pressures(self): + return self.get_latest_station_measures(_STATION_PRESSURE_TYPE) - def get24hRain(self): - return self.getAccessoryMeasures(_ACCESSORY_RAIN_24H_TYPE) + def get_average_pressure(self): + return average_measure(self.get_latest_pressures()) - def getAverage24hRain(self): - return averageMeasure(self.get24hRain()) + def get_latest_temperatures(self): + return self.get_latest_station_measures(_STATION_TEMPERATURE_TYPE) - def getLatestPressures(self): - return self.getLatestStationMeasures(_STATION_PRESSURE_TYPE) + def get_average_temperature(self): + return average_measure(self.get_latest_temperatures()) - def getAveragePressure(self): - return averageMeasure(self.getLatestPressures()) + def get_latest_humidities(self): + return self.get_latest_station_measures(_STATION_HUMIDITY_TYPE) - def getLatestTemperatures(self): - return self.getLatestStationMeasures(_STATION_TEMPERATURE_TYPE) + def get_average_humidity(self): + return average_measure(self.get_latest_humidities()) - def getAverageTemperature(self): - return averageMeasure(self.getLatestTemperatures()) + def get_latest_wind_strengths(self): + return self.get_accessory_measures(_ACCESSORY_WIND_STRENGTH_TYPE) - def getLatestHumidities(self): - return self.getLatestStationMeasures(_STATION_HUMIDITY_TYPE) + def get_average_wind_strength(self): + return average_measure(self.get_latest_wind_strengths()) - def getAverageHumidity(self): - return averageMeasure(self.getLatestHumidities()) + def get_latest_wind_angles(self): + return self.get_accessory_measures(_ACCESSORY_WIND_ANGLE_TYPE) - def getLatestWindStrengths(self): - return self.getAccessoryMeasures(_ACCESSORY_WIND_STRENGTH_TYPE) + def get_latest_gust_strengths(self): + return self.get_accessory_measures(_ACCESSORY_GUST_STRENGTH_TYPE) - def getAverageWindStrength(self): - return averageMeasure(self.getLatestWindStrengths()) + def get_average_gust_strength(self): + return average_measure(self.get_latest_gust_strengths()) - def getLatestWindAngles(self): - return self.getAccessoryMeasures(_ACCESSORY_WIND_ANGLE_TYPE) + def get_latest_gust_angles(self): + return self.get_accessory_measures(_ACCESSORY_GUST_ANGLE_TYPE) - def getLatestGustStrengths(self): - return self.getAccessoryMeasures(_ACCESSORY_GUST_STRENGTH_TYPE) - - def getAverageGustStrength(self): - return averageMeasure(self.getLatestGustStrengths()) - - def getLatestGustAngles(self): - return self.getAccessoryMeasures(_ACCESSORY_GUST_ANGLE_TYPE) - - def getLocations(self): + def get_locations(self): locations = {} for station in self.raw_data: locations[station["_id"]] = station["place"]["location"] return locations - # Backwards compatibility for < 1.2 - def getTimeforMeasure(self): - return self.getTimeForRainMeasures() - - def getTimeForRainMeasures(self): - return self.getAccessoryMeasures(_ACCESSORY_RAIN_TIME_TYPE) + def get_time_for_rain_measures(self): + return self.get_accessory_measures(_ACCESSORY_RAIN_TIME_TYPE) - def getTimeForWindMeasures(self): - return self.getAccessoryMeasures(_ACCESSORY_WIND_TIME_TYPE) + def get_time_for_wind_measures(self): + return self.get_accessory_measures(_ACCESSORY_WIND_TIME_TYPE) - def getLatestStationMeasures(self, data_type): + def get_latest_station_measures(self, data_type): measures = {} for station in self.raw_data: - for _, module in station["measures"].items(): + for module in station["measures"].values(): if ( "type" in module and data_type in module["type"] @@ -156,16 +140,16 @@ def getLatestStationMeasures(self, data_type): ] return measures - def getAccessoryMeasures(self, data_type): + def get_accessory_measures(self, data_type): measures = {} for station in self.raw_data: - for _, module in station["measures"].items(): + for module in station["measures"].values(): if data_type in module: measures[station["_id"]] = module[data_type] return measures -def averageMeasure(measures): +def average_measure(measures): if measures: return sum(measures.values()) / len(measures) return 0.0 diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 52262e96b..b23100480 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -1,16 +1,16 @@ import logging -from .exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule -from .helpers import _BASE_URL +from pyatmo.exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule +from pyatmo.helpers import BASE_URL LOG = logging.getLogger(__name__) -_GETHOMESDATA_REQ = _BASE_URL + "api/homesdata" -_GETHOMESTATUS_REQ = _BASE_URL + "api/homestatus" -_SETTHERMMODE_REQ = _BASE_URL + "api/setthermmode" -_SETROOMTHERMPOINT_REQ = _BASE_URL + "api/setroomthermpoint" -_GETROOMMEASURE_REQ = _BASE_URL + "api/getroommeasure" -_SWITCHHOMESCHEDULE_REQ = _BASE_URL + "api/switchhomeschedule" +_GETHOMESDATA_REQ = BASE_URL + "api/homesdata" +_GETHOMESTATUS_REQ = BASE_URL + "api/homestatus" +_SETTHERMMODE_REQ = BASE_URL + "api/setthermmode" +_SETROOMTHERMPOINT_REQ = BASE_URL + "api/setroomthermpoint" +_GETROOMMEASURE_REQ = BASE_URL + "api/getroommeasure" +_SWITCHHOMESCHEDULE_REQ = BASE_URL + "api/switchhomeschedule" class HomeData: @@ -18,72 +18,73 @@ class HomeData: List the Energy devices (relays, thermostat modules and valves) Args: - authData (ClientAuth): Authentication information with a working access Token + auth_data (ClientAuth): Authentication information with a working access Token """ - def __init__(self, authData): - self.authData = authData - resp = self.authData.post_request(url=_GETHOMESDATA_REQ) + def __init__(self, auth_data): + self.auth_data = auth_data + resp = self.auth_data.post_request(url=_GETHOMESDATA_REQ) if resp is None or "body" not in resp: raise NoDevice("No thermostat data returned by Netatmo server") - self.rawData = resp["body"].get("homes") - if not self.rawData: + self.raw_data = resp["body"].get("homes") + if not self.raw_data: raise NoDevice("No thermostat data available") - self.homes = {d["id"]: d for d in self.rawData} + self.homes = {d["id"]: d for d in self.raw_data} if not self.homes: raise NoDevice("No thermostat available") self.modules = {} self.rooms = {} self.schedules = {} self.zones = {} - self.setpoint_duration = {} + self.set_point_duration = {} self.default_home = None self.default_home_id = None - for item in self.rawData: - idHome = item.get("id") - if not idHome: + for item in self.raw_data: + id_home = item.get("id") + if not id_home: LOG.error('No key ["id"] in %s', item.keys()) continue - nameHome = item.get("name") - if not nameHome: - nameHome = "Unknown" - self.homes[idHome]["name"] = nameHome + home_name = item.get("name") + if not home_name: + home_name = "Unknown" + self.homes[id_home]["name"] = home_name if "modules" in item: - if idHome not in self.modules: - self.modules[idHome] = {} - for m in item["modules"]: - self.modules[idHome][m["id"]] = m - if idHome not in self.rooms: - self.rooms[idHome] = {} - if idHome not in self.schedules: - self.schedules[idHome] = {} - if idHome not in self.zones: - self.zones[idHome] = {} - if idHome not in self.setpoint_duration: - self.setpoint_duration[idHome] = {} + if id_home not in self.modules: + self.modules[id_home] = {} + for module in item["modules"]: + self.modules[id_home][module["id"]] = module + + if id_home not in self.rooms: + self.rooms[id_home] = {} + if id_home not in self.schedules: + self.schedules[id_home] = {} + if id_home not in self.zones: + self.zones[id_home] = {} + if id_home not in self.set_point_duration: + self.set_point_duration[id_home] = {} if "therm_setpoint_default_duration" in item: - self.setpoint_duration[idHome] = item[ + self.set_point_duration[id_home] = item[ "therm_setpoint_default_duration" ] if "rooms" in item: for room in item["rooms"]: - self.rooms[idHome][room["id"]] = room + self.rooms[id_home][room["id"]] = room if "therm_schedules" in item: - self.default_home = nameHome + self.default_home = home_name self.default_home_id = item["id"] for schedule in item["therm_schedules"]: - self.schedules[idHome][schedule["id"]] = schedule + self.schedules[id_home][schedule["id"]] = schedule for schedule in item["therm_schedules"]: - scheduleId = schedule["id"] - if scheduleId not in self.zones[idHome]: - self.zones[idHome][scheduleId] = {} + schedule_id = schedule["id"] + if schedule_id not in self.zones[id_home]: + self.zones[id_home][schedule_id] = {} for zone in schedule["zones"]: - self.zones[idHome][scheduleId][zone["id"]] = zone + self.zones[id_home][schedule_id][zone["id"]] = zone - def homeById(self, hid): + def home_by_id(self, hid): return None if hid not in self.homes else self.homes[hid] - def homeByName(self, home=None): + def home_by_name(self, home=None): if not home: home = self.default_home for key, value in self.homes.items(): @@ -91,7 +92,7 @@ def homeByName(self, home=None): return self.homes[key] raise InvalidHome("Invalid Home %s" % home) - def gethomeId(self, home=None): + def get_home_id(self, home=None): if not home: home = self.default_home for key, value in self.homes.items(): @@ -100,22 +101,23 @@ def gethomeId(self, home=None): return self.homes[key]["id"] raise InvalidHome("Invalid Home %s" % home) - def getHomeName(self, home_id=None): + def get_home_name(self, home_id=None): if home_id is None: home_id = self.default_home_id + for key, value in self.homes.items(): if value["id"] == home_id: return self.homes[key]["name"] raise InvalidHome("Invalid Home ID %s" % home_id) - def getSelectedschedule(self, home=None, home_id=None): + def get_selected_schedule(self, home=None, home_id=None): if not home_id: if not home: home = self.default_home - home_id = self.gethomeId(home=home) - return self.get_selected_schedule(home_id=home_id) + home_id = self.get_home_id(home=home) + return self._get_selected_schedule(home_id=home_id) - def get_selected_schedule(self, home_id: str): + def _get_selected_schedule(self, home_id: str): """Get the selected schedule for a given home ID.""" try: schedules = self.schedules[home_id] @@ -126,10 +128,10 @@ def get_selected_schedule(self, home_id: str): if "selected" in schedules[key].keys(): return schedules[key] - def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): + def switch_home_schedule(self, schedule_id=None, schedule=None, home=None): if home is None: home = self.default_home - home_id = self.gethomeId(home=home) + home_id = self.get_home_id(home=home) schedules = { self.schedules[home_id][s]["name"]: self.schedules[home_id][s]["id"] @@ -145,13 +147,11 @@ def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): else: raise NoSchedule("No schedule specified") - return self.switch_home_schedule(schedule_id=schedule_id, home_id=home_id) + return self._switch_home_schedule(schedule_id=schedule_id, home_id=home_id) - def switch_home_schedule(self, schedule_id: str, home_id: str) -> bool: + def _switch_home_schedule(self, schedule_id: str, home_id: str) -> None: """.""" - try: - schedules = self.schedules[home_id] - except KeyError: + if home_id not in self.schedules: raise NoSchedule("No schedules available for %s" % home_id) schedules = { @@ -161,33 +161,31 @@ def switch_home_schedule(self, schedule_id: str, home_id: str) -> bool: if schedule_id not in list(schedules.values()): raise NoSchedule("%s is not a valid schedule id" % schedule_id) - postParams = { - "home_id": home_id, - "schedule_id": schedule_id, - } - resp = self.authData.post_request( - url=_SWITCHHOMESCHEDULE_REQ, params=postParams + post_params = {"home_id": home_id, "schedule_id": schedule_id} + resp = self.auth_data.post_request( + url=_SWITCHHOMESCHEDULE_REQ, params=post_params ) LOG.debug("Response: %s", resp) class HomeStatus: - def __init__(self, authData, home_data=None, home_id=None, home=None): - self.authData = authData + def __init__(self, auth_data, home_data=None, home_id=None, home=None): + self.auth_data = auth_data if home_data is None: - self.home_data = HomeData(authData) + self.home_data = HomeData(auth_data) else: self.home_data = home_data if home_id is not None: self.home_id = home_id elif home is not None: - self.home_id = self.home_data.gethomeId(home=home) + self.home_id = self.home_data.get_home_id(home=home) else: - self.home_id = self.home_data.gethomeId(home=self.home_data.default_home) - postParams = {"home_id": self.home_id} + self.home_id = self.home_data.get_home_id(home=self.home_data.default_home) + + post_params = {"home_id": self.home_id} - resp = self.authData.post_request(url=_GETHOMESTATUS_REQ, params=postParams) + resp = self.auth_data.post_request(url=_GETHOMESTATUS_REQ, params=post_params) if ( "errors" in resp or "body" not in resp @@ -196,29 +194,30 @@ def __init__(self, authData, home_data=None, home_id=None, home=None): ): LOG.error("Errors in response: %s", resp) raise NoDevice("No device found, errors in response") - self.rawData = resp["body"]["home"] + self.raw_data = resp["body"]["home"] self.rooms = {} self.thermostats = {} self.valves = {} self.relays = {} - for r in self.rawData.get("rooms", []): - self.rooms[r["id"]] = r - for module in self.rawData.get("modules", []): + for room in self.raw_data.get("rooms", []): + self.rooms[room["id"]] = room + + for module in self.raw_data.get("modules", []): if module["type"] == "NATherm1": - thermostatId = module["id"] - if thermostatId not in self.thermostats: - self.thermostats[thermostatId] = {} - self.thermostats[thermostatId] = module + thermostat_id = module["id"] + if thermostat_id not in self.thermostats: + self.thermostats[thermostat_id] = {} + self.thermostats[thermostat_id] = module elif module["type"] == "NRV": - valveId = module["id"] - if valveId not in self.valves: - self.valves[valveId] = {} - self.valves[valveId] = module + valve_id = module["id"] + if valve_id not in self.valves: + self.valves[valve_id] = {} + self.valves[valve_id] = module elif module["type"] == "NAPlug": - relayId = module["id"] - if relayId not in self.relays: - self.relays[relayId] = {} - self.relays[relayId] = module + relay_id = module["id"] + if relay_id not in self.relays: + self.relays[relay_id] = {} + self.relays[relay_id] = module if self.rooms != {}: self.default_room = list(self.rooms.values())[0] if self.relays != {}: @@ -228,7 +227,7 @@ def __init__(self, authData, home_data=None, home_id=None, home=None): if self.valves != {}: self.default_valve = list(self.valves.values())[0] - def roomById(self, rid): + def room_by_id(self, rid): if not rid: return self.default_room for key, value in self.rooms.items(): @@ -236,7 +235,7 @@ def roomById(self, rid): return self.rooms[key] raise InvalidRoom("No room with ID %s" % rid) - def thermostatById(self, rid): + def thermostat_by_id(self, rid): if not rid: return self.default_thermostat for key, value in self.thermostats.items(): @@ -244,7 +243,7 @@ def thermostatById(self, rid): return self.thermostats[key] raise InvalidRoom("No room with ID %s" % rid) - def relayById(self, rid): + def relay_by_id(self, rid): if not rid: return self.default_relay for key, value in self.relays.items(): @@ -252,7 +251,7 @@ def relayById(self, rid): return self.relays[key] raise InvalidRoom("No room with ID %s" % rid) - def valveById(self, rid): + def valve_by_id(self, rid): if not rid: return self.default_valve for key, value in self.valves.items(): @@ -260,23 +259,23 @@ def valveById(self, rid): return self.valves[key] raise InvalidRoom("No room with ID %s" % rid) - def setPoint(self, rid=None): + def set_point(self, rid=None): """ Return the setpoint of a given room. """ setpoint = None - room_data = self.roomById(rid=rid) + room_data = self.room_by_id(rid=rid) if room_data: setpoint = room_data["therm_setpoint_temperature"] return setpoint - def setPointmode(self, rid=None): + def set_point_mode(self, rid=None): """ Return the setpointmode of a given room. """ setpointmode = None try: - room_data = self.roomById(rid=rid) + room_data = self.room_by_id(rid=rid) except InvalidRoom: LOG.debug("Invalid room %s", rid) room_data = None @@ -284,58 +283,57 @@ def setPointmode(self, rid=None): setpointmode = room_data["therm_setpoint_mode"] return setpointmode - def getAwaytemp(self, home=None, home_id=None): + def get_away_temp(self, home=None, home_id=None): if not home_id: if not home: home = self.home_data.default_home try: - home_id = self.home_data.gethomeId(home) + home_id = self.home_data.get_home_id(home) except InvalidHome: LOG.debug("No Schedule for Home ID %s", home_id) return None try: - data = self.home_data.getSelectedschedule(home_id=home_id) + data = self.home_data.get_selected_schedule(home_id=home_id) except NoSchedule: LOG.debug("No Schedule for Home ID %s", home_id) return None return data["away_temp"] - def getHgtemp(self, home=None, home_id=None): + def get_hg_temp(self, home=None, home_id=None): if not home_id: if not home: home = self.home_data.default_home - home_id = self.home_data.gethomeId(home) + home_id = self.home_data.get_home_id(home) try: - data = self.home_data.getSelectedschedule(home_id=home_id) + data = self.home_data.get_selected_schedule(home_id=home_id) except NoSchedule: LOG.debug("No Schedule for Home ID %s", home_id) return None return data["hg_temp"] - def measuredTemperature(self, rid=None): + def measured_temperature(self, rid=None): """ Return the measured temperature of a given room. """ temperature = None - room_data = self.roomById(rid=rid) + room_data = self.room_by_id(rid=rid) if room_data: temperature = room_data.get("therm_measured_temperature") return temperature - def boilerStatus(self, rid=None): + def boiler_status(self, rid=None): boiler_status = None if rid: - relay_status = self.thermostatById(rid=rid) + relay_status = self.thermostat_by_id(rid=rid) else: - relay_status = self.thermostatById(rid=None) + relay_status = self.thermostat_by_id(rid=None) if relay_status: boiler_status = relay_status["boiler_status"] return boiler_status - def thermostatType(self, home, rid, home_id=None): - module_id = None + def thermostat_type(self, home, rid, home_id=None): if home_id is None: - home_id = self.home_data.gethomeId(home=home) + home_id = self.home_data.get_home_id(home=home) for key in self.home_data.rooms[home_id]: if key == rid: for module_id in self.home_data.rooms[home_id][rid]["module_ids"]: @@ -344,7 +342,7 @@ def thermostatType(self, home, rid, home_id=None): if module_id in self.valves: return "NRV" - def setThermmode(self, home_id, mode, end_time=None, schedule_id=None): + def set_therm_mode(self, home_id, mode, end_time=None, schedule_id=None): postParams = { "home_id": home_id, "mode": mode, @@ -353,10 +351,10 @@ def setThermmode(self, home_id, mode, end_time=None, schedule_id=None): postParams["endtime"] = end_time if schedule_id is not None and mode == "schedule": postParams["schedule_id"] = schedule_id - return self.authData.post_request(url=_SETTHERMMODE_REQ, params=postParams) + return self.auth_data.post_request(url=_SETTHERMMODE_REQ, params=postParams) - def setroomThermpoint(self, home_id, room_id, mode, temp=None, end_time=None): - postParams = { + def set_room_therm_point(self, home_id, room_id, mode, temp=None, end_time=None): + post_params = { "home_id": home_id, "room_id": room_id, "mode": mode, @@ -364,7 +362,9 @@ def setroomThermpoint(self, home_id, room_id, mode, temp=None, end_time=None): # Temp and endtime should only be send when mode=='manual', but netatmo api can # handle that even when mode == 'home' and these settings don't make sense if temp is not None: - postParams["temp"] = temp + post_params["temp"] = temp if end_time is not None: - postParams["endtime"] = end_time - return self.authData.post_request(url=_SETROOMTHERMPOINT_REQ, params=postParams) + post_params["endtime"] = end_time + return self.auth_data.post_request( + url=_SETROOMTHERMPOINT_REQ, params=post_params + ) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 1fab6a07c..97b144ee5 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -1,77 +1,84 @@ import logging import time -from .exceptions import NoDevice -from .helpers import _BASE_URL, fixId, todayStamps +from pyatmo.exceptions import NoDevice +from pyatmo.helpers import BASE_URL, fixId, today_stamps LOG = logging.getLogger(__name__) -_GETMEASURE_REQ = _BASE_URL + "api/getmeasure" -_GETSTATIONDATA_REQ = _BASE_URL + "api/getstationsdata" +_GETMEASURE_REQ = BASE_URL + "api/getmeasure" +_GETSTATIONDATA_REQ = BASE_URL + "api/getstationsdata" class WeatherStationData: """ List the Weather Station devices (stations and modules) Args: - authData (ClientAuth): Authentication information with a working access Token + auth_data (ClientAuth): Authentication information with a working access Token """ - def __init__(self, authData, urlReq=None): + def __init__(self, auth_data, url_req=None): """Initialize the weather station class.""" - self.urlReq = urlReq or _GETSTATIONDATA_REQ - self.authData = authData - resp = self.authData.post_request(url=self.urlReq) + self.url_req = url_req or _GETSTATIONDATA_REQ + self.auth_data = auth_data + + resp = self.auth_data.post_request(url=self.url_req) if resp is None or "body" not in resp: raise NoDevice("No weather station data returned by Netatmo server") try: - self.rawData = fixId(resp["body"].get("devices")) + self.raw_data = fixId(resp["body"].get("devices")) except KeyError: LOG.debug("No in response %s", resp) raise NoDevice("No weather station data returned by Netatmo server") - if not self.rawData: + + if not self.raw_data: raise NoDevice("No weather station available") - self.stations = {d["_id"]: d for d in self.rawData} + + self.stations = {d["_id"]: d for d in self.raw_data} self.modules = {} - for item in self.rawData: + for item in self.raw_data: if "modules" not in item: item["modules"] = [item] - for m in item["modules"]: - if "module_name" not in m: - if m["type"] == "NHC": - m["module_name"] = m["station_name"] + + for module in item["modules"]: + if "module_name" not in module: + if module["type"] == "NHC": + module["module_name"] = module["station_name"] else: continue - self.modules[m["_id"]] = m - self.modules[m["_id"]]["main_device"] = item["_id"] + self.modules[module["_id"]] = module + self.modules[module["_id"]]["main_device"] = item["_id"] + self.default_station = list(self.stations.values())[0]["station_name"] - def modulesNamesList(self, station=None, station_id=None): + def modules_names_list(self, station_name=None, station_id=None): """Return a list of all modules for a given or all stations.""" res = set() station_data = None if station_id is not None: - station_data = self.stationById(station_id) - elif station is not None: - station_data = self.stationByName(station) + station_data = self.station_by_id(station_id) + elif station_name is not None: + station_data = self.station_by_name(station_name) + if station_data is not None: res.add(station_data.get("module_name", station_data.get("type"))) - for m in station_data["modules"]: - res.add(m.get("module_name", m.get("type"))) + for module in station_data["modules"]: + res.add(module.get("module_name", module.get("type"))) else: res.update([m["module_name"] for m in self.modules.values()]) - for s in self.stations.values(): - res.add(s.get("module_name", s.get("type"))) + for station in self.stations.values(): + res.add(station.get("module_name", station.get("type"))) return list(res) - def getModules(self, station=None, station_id=None): + def get_modules(self, station_name=None, station_id=None): """Return a dict or modules for a given or all stations.""" res = {} station_data = None if station_id is not None: - station_data = self.stationById(station_id) - elif station is not None: - station_data = self.stationByName(station) + station_data = self.station_by_id(station_id) + elif station_name is not None: + station_data = self.station_by_name(station_name) + if station_data is not None: stations = [self.stations[station_data["_id"]]] else: @@ -91,62 +98,64 @@ def getModules(self, station=None, station_id=None): } return res - def stationByName(self, station=None): + def station_by_name(self, station_name=None): """Return station by name.""" - if not station: - station = self.default_station - for i, s in self.stations.items(): - if s["station_name"] == station: - return self.stations[i] + if not station_name: + station_name = self.default_station + + for station_data in self.stations.values(): + if station_data["station_name"] == station_name: + return station_data return None - def stationById(self, sid): + def station_by_id(self, sid): """Return station by id.""" return None if sid not in self.stations else self.stations[sid] - def moduleByName(self, module_name, station=None): + def module_by_name(self, module_name, station_name=None): """Return module by name.""" - s = None - if station: - s = self.stationByName(station) - if not s: + station = None + if station_name: + station = self.station_by_name(station_name) + if not station: return None - if s["module_name"] == module_name: - return s + if station["module_name"] == module_name: + return station + else: - for s in self.stations.values(): - if "module_name" in s: - if s["module_name"] == module_name: - return s + for station in self.stations.values(): + if "module_name" in station: + if station["module_name"] == module_name: + return station break - for m in self.modules: - module = self.modules[m] + + for module in self.modules.values(): if module["module_name"] == module_name: - if not s or module["main_device"] == s["_id"]: + if not station or module["main_device"] == station["_id"]: return module return None - def moduleById(self, mid, sid=None): + def module_by_id(self, mid, sid=None): """Return module by id.""" - s = self.stationById(sid) if sid else None + station_data = self.station_by_id(sid) if sid else None if mid in self.modules: - if s: - for module in s["modules"]: + if station_data is not None: + for module in station_data.get("modules"): if module["_id"] == mid: return module else: return self.modules[mid] - def monitoredConditions(self, module=None, moduleId=None): + def monitored_conditions(self, module=None, module_id=None): """Return monitored conditions for given module(s).""" - if moduleId: - mod = self.moduleById(moduleId) + if module_id: + mod = self.module_by_id(module_id) if not mod: - mod = self.stationById(moduleId) + mod = self.station_by_id(module_id) elif module: - mod = self.moduleByName(module) + mod = self.module_by_name(module) if not mod: - mod = self.stationByName(module) + mod = self.station_by_name(module) else: return None conditions = [] @@ -181,38 +190,51 @@ def monitoredConditions(self, module=None, moduleId=None): conditions.append("reachable") return conditions - def lastData(self, station=None, exclude=0, byId=False): + def last_data(self, station=None, exclude=0, by_id=False): """Return data for a given station and time frame.""" - key = "_id" if byId else "module_name" + key = "_id" if by_id else "module_name" if station is not None: stations = [station] - elif byId: - stations = [s["_id"] for s in list(self.stations.values())] + elif by_id: + stations = [s["_id"] for s in self.stations.values()] else: - stations = [s["station_name"] for s in list(self.stations.values())] + stations = [s["station_name"] for s in self.stations.values()] + # Breaking change from Netatmo : dashboard_data no longer available if station lost - lastD = {} - for st in stations: - s = self.stationById(st) if byId else self.stationByName(st) - if not s or "dashboard_data" not in s: - LOG.info("No dashboard data for station %s", st) + last_data = {} + for _station in stations: + station_data = ( + self.station_by_id(_station) + if by_id + else self.station_by_name(_station) + ) + if not station_data or "dashboard_data" not in station_data: + LOG.info("No dashboard data for station %s", _station) continue + # Define oldest acceptable sensor measure event limit = (time.time() - exclude) if exclude else 0 - ds = s["dashboard_data"] - if key in s and ds["time_utc"] > limit: - lastD[s[key]] = ds.copy() - lastD[s[key]]["When"] = lastD[s[key]].pop("time_utc") - lastD[s[key]]["wifi_status"] = s["wifi_status"] - lastD[s[key]]["reachable"] = s["reachable"] - for module in s["modules"]: + dashboard_data = station_data["dashboard_data"] + if key in station_data and dashboard_data["time_utc"] > limit: + last_data[station_data[key]] = dashboard_data.copy() + last_data[station_data[key]]["When"] = last_data[station_data[key]].pop( + "time_utc" + ) + last_data[station_data[key]]["wifi_status"] = station_data[ + "wifi_status" + ] + last_data[station_data[key]]["reachable"] = station_data["reachable"] + + for module in station_data["modules"]: if "dashboard_data" not in module or key not in module: continue - ds = module["dashboard_data"] - if "time_utc" in ds and ds["time_utc"] > limit: - lastD[module[key]] = ds.copy() - lastD[module[key]]["When"] = lastD[module[key]].pop("time_utc") - # For potential use, add battery and radio coverage information to module data if present + dashboard_data = module["dashboard_data"] + if "time_utc" in dashboard_data and dashboard_data["time_utc"] > limit: + last_data[module[key]] = dashboard_data.copy() + last_data[module[key]]["When"] = last_data[module[key]].pop( + "time_utc" + ) + for i in ( "rf_status", "battery_vp", @@ -221,28 +243,28 @@ def lastData(self, station=None, exclude=0, byId=False): "wifi_status", ): if i in module: - lastD[module[key]][i] = module[i] - return lastD + last_data[module[key]][i] = module[i] + return last_data - def checkNotUpdated(self, station=None, delay=3600): + def check_not_updated(self, station=None, delay=3600): """Check if a given station has not been updated.""" - res = self.lastData(station) + res = self.last_data(station) ret = [] - for mn, v in res.items(): - if time.time() - v["When"] > delay: - ret.append(mn) + for i, value_data in res.items(): + if time.time() - value_data["When"] > delay: + ret.append(i) return ret if ret else None - def checkUpdated(self, station=None, delay=3600): + def check_updated(self, station=None, delay=3600): """Check if a given station has been updated.""" - res = self.lastData(station) + res = self.last_data(station) ret = [] - for mn, v in res.items(): - if time.time() - v["When"] < delay: - ret.append(mn) + for i, value_data in res.items(): + if time.time() - value_data["When"] < delay: + ret.append(i) return ret if ret else None - def getMeasure( + def get_measure( self, device_id, scale, @@ -254,59 +276,67 @@ def getMeasure( optimize=False, real_time=False, ): - postParams = {"device_id": device_id} + post_params = {"device_id": device_id} if module_id: - postParams["module_id"] = module_id - postParams["scale"] = scale - postParams["type"] = mtype + post_params["module_id"] = module_id + post_params["scale"] = scale + post_params["type"] = mtype if date_begin: - postParams["date_begin"] = date_begin + post_params["date_begin"] = date_begin if date_end: - postParams["date_end"] = date_end + post_params["date_end"] = date_end if limit: - postParams["limit"] = limit - postParams["optimize"] = "true" if optimize else "false" - postParams["real_time"] = "true" if real_time else "false" - return self.authData.post_request(url=_GETMEASURE_REQ, params=postParams) + post_params["limit"] = limit + post_params["optimize"] = "true" if optimize else "false" + post_params["real_time"] = "true" if real_time else "false" + return self.auth_data.post_request(url=_GETMEASURE_REQ, params=post_params) - def MinMaxTH(self, station=None, module=None, frame="last24"): + def min_max_th(self, station=None, module=None, frame="last24"): if not station: station = self.default_station - s = self.stationByName(station) - if not s: - s = self.stationById(station) - if not s: + station_data = self.station_by_name(station) + if not station_data: + station_data = self.station_by_id(station) + if not station_data: return None + if frame == "last24": end = time.time() start = end - 24 * 3600 # 24 hours ago elif frame == "day": - start, end = todayStamps() - if module and module != s["module_name"]: - m = self.moduleByName(module, s["station_name"]) - if not m: - m = self.moduleById(s["_id"], module) - if not m: + start, end = today_stamps() + + if module and module != station_data["module_name"]: + module_data = self.module_by_name(module, station_data["station_name"]) + if not module_data: + module_data = self.module_by_id(station_data["_id"], module) + if not module_data: return None # retrieve module's data - resp = self.getMeasure( - device_id=s["_id"], - module_id=m["_id"], + resp = self.get_measure( + device_id=station_data["_id"], + module_id=module_data["_id"], scale="max", mtype="Temperature,Humidity", date_begin=start, date_end=end, ) else: # retrieve station's data - resp = self.getMeasure( - device_id=s["_id"], + resp = self.get_measure( + device_id=station_data["_id"], scale="max", mtype="Temperature,Humidity", date_begin=start, date_end=end, ) + if resp: - T = [v[0] for v in resp["body"].values()] - H = [v[1] for v in resp["body"].values()] - return min(T), max(T), min(H), max(H) + temperatures = [v[0] for v in resp["body"].values()] + humidities = [v[1] for v in resp["body"].values()] + return ( + min(temperatures), + max(temperatures), + min(humidities), + max(humidities), + ) return None diff --git a/tests/conftest.py b/tests/conftest.py index 56abaa407..76ce7fe25 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """Define shared fixtures.""" +# pylint: disable=redefined-outer-name, protected-access import json from contextlib import contextmanager @@ -14,16 +15,16 @@ def does_not_raise(): @pytest.fixture(scope="function") def auth(requests_mock): - with open("fixtures/oauth2_token.json") as f: - json_fixture = json.load(f) + with open("fixtures/oauth2_token.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( - pyatmo.auth._AUTH_REQ, + pyatmo.auth.AUTH_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) authorization = pyatmo.ClientAuth( - clientId="CLIENT_ID", - clientSecret="CLIENT_SECRET", + client_id="CLIENT_ID", + client_secret="CLIENT_SECRET", username="USERNAME", password="PASSWORD", scope=" ".join(pyatmo.auth.ALL_SCOPES), @@ -32,9 +33,9 @@ def auth(requests_mock): @pytest.fixture(scope="function") -def homeData(auth, requests_mock): - with open("fixtures/home_data_simple.json") as f: - json_fixture = json.load(f) +def home_data(auth, requests_mock): + with open("fixtures/home_data_simple.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, @@ -44,16 +45,16 @@ def homeData(auth, requests_mock): @pytest.fixture(scope="function") -def homeStatus(auth, requests_mock): - with open("fixtures/home_status_simple.json") as f: - json_fixture = json.load(f) +def home_status(auth, requests_mock): + with open("fixtures/home_status_simple.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESTATUS_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with open("fixtures/home_data_simple.json") as f: - json_fixture = json.load(f) + with open("fixtures/home_data_simple.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, @@ -63,9 +64,9 @@ def homeStatus(auth, requests_mock): @pytest.fixture(scope="function") -def publicData(auth, requests_mock): - with open("fixtures/public_data_simple.json") as f: - json_fixture = json.load(f) +def public_data(auth, requests_mock): + with open("fixtures/public_data_simple.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.public_data._GETPUBLIC_DATA, json=json_fixture, @@ -75,9 +76,9 @@ def publicData(auth, requests_mock): @pytest.fixture(scope="function") -def weatherStationData(auth, requests_mock): - with open("fixtures/weatherstation_data_simple.json") as f: - json_fixture = json.load(f) +def weather_station_data(auth, requests_mock): + with open("fixtures/weatherstation_data_simple.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.weather_station._GETSTATIONDATA_REQ, json=json_fixture, @@ -87,9 +88,9 @@ def weatherStationData(auth, requests_mock): @pytest.fixture(scope="function") -def homeCoachData(auth, requests_mock): - with open("fixtures/home_coach_simple.json") as f: - json_fixture = json.load(f) +def home_coach_data(auth, requests_mock): + with open("fixtures/home_coach_simple.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.home_coach._GETHOMECOACHDATA_REQ, json=json_fixture, @@ -99,9 +100,9 @@ def homeCoachData(auth, requests_mock): @pytest.fixture(scope="function") -def cameraHomeData(auth, requests_mock): - with open("fixtures/camera_home_data.json") as f: - json_fixture = json.load(f) +def camera_home_data(auth, requests_mock): + with open("fixtures/camera_home_data.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.camera._GETHOMEDATA_REQ, json=json_fixture, diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index 8044699a3..be0d12150 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -1,4 +1,5 @@ """Define tests for untility methods.""" +# pylint: disable=protected-access import json import time @@ -8,7 +9,7 @@ import pyatmo -def test_ClientAuth(auth): +def test_client_auth(auth): assert auth._oauth.token["access_token"] == ( "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12" ) @@ -17,58 +18,58 @@ def test_ClientAuth(auth): ) -def test_ClientAuth_invalid(requests_mock): - with open("fixtures/invalid_grant.json") as f: - json_fixture = json.load(f) +def test_client_auth_invalid(requests_mock): + with open("fixtures/invalid_grant.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( - pyatmo.auth._AUTH_REQ, + pyatmo.auth.AUTH_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) with pytest.raises(oauthlib.oauth2.rfc6749.errors.InvalidGrantError): pyatmo.ClientAuth( - clientId="CLIENT_ID", - clientSecret="CLIENT_SECRET", + client_id="CLIENT_ID", + client_secret="CLIENT_SECRET", username="USERNAME", password="PASSWORD", ) -def test_postRequest_json(auth, requests_mock): +def test_post_request_json(auth, requests_mock): """Test wrapper for posting requests against the Netatmo API.""" requests_mock.post( - pyatmo.auth._BASE_URL, + pyatmo.helpers.BASE_URL, json={"a": "b"}, headers={"content-type": "application/json"}, ) - resp = auth.post_request(pyatmo.auth._BASE_URL, None) + resp = auth.post_request(pyatmo.helpers.BASE_URL, None) assert resp == {"a": "b"} -def test_postRequest_binary(auth, requests_mock): +def test_post_request_binary(auth, requests_mock): """Test wrapper for posting requests against the Netatmo API.""" requests_mock.post( - pyatmo.helpers._BASE_URL, + pyatmo.helpers.BASE_URL, text="Success", headers={"content-type": "application/text"}, ) - resp = auth.post_request(pyatmo.helpers._BASE_URL, None) + resp = auth.post_request(pyatmo.helpers.BASE_URL, None) assert resp == b"Success" @pytest.mark.parametrize( "test_input,expected", [(200, None), (404, None), (401, None)], ) -def test_postRequest_fail(auth, requests_mock, test_input, expected): +def test_post_request_fail(auth, requests_mock, test_input, expected): """Test failing requests against the Netatmo API.""" - requests_mock.post(pyatmo.helpers._BASE_URL, status_code=test_input) + requests_mock.post(pyatmo.helpers.BASE_URL, status_code=test_input) if test_input == 200: - resp = auth.post_request(pyatmo.helpers._BASE_URL, None) + resp = auth.post_request(pyatmo.helpers.BASE_URL, None) assert resp is expected else: with pytest.raises(pyatmo.ApiError): - resp = auth.post_request(pyatmo.helpers._BASE_URL, None) + resp = auth.post_request(pyatmo.helpers.BASE_URL, None) @pytest.mark.parametrize( @@ -84,9 +85,9 @@ def test_postRequest_fail(auth, requests_mock, test_input, expected): pytest.param({1}, None, marks=pytest.mark.xfail), ], ) -def test_toTimeString(test_input, expected): +def test_to_time_string(test_input, expected): """Test time to string conversion.""" - assert pyatmo.helpers.toTimeString(test_input) == expected + assert pyatmo.helpers.to_time_string(test_input) == expected @pytest.mark.parametrize( @@ -98,9 +99,9 @@ def test_toTimeString(test_input, expected): ("2033-05-18_03:33:20", 2000000000), ], ) -def test_toEpoch(test_input, expected): +def test_to_epoch(test_input, expected): """Test time to epoch conversion.""" - assert pyatmo.helpers.toEpoch(test_input) == expected + assert pyatmo.helpers.to_epoch(test_input) == expected @pytest.mark.parametrize( @@ -111,11 +112,11 @@ def test_toEpoch(test_input, expected): pytest.param("2000-04-31", None, marks=pytest.mark.xfail), ], ) -def test_todayStamps(monkeypatch, test_input, expected): - """Test todayStamps function.""" +def test_today_stamps(monkeypatch, test_input, expected): + """Test today_stamps function.""" - def mockreturn(format): + def mockreturn(_): return test_input monkeypatch.setattr(time, "strftime", mockreturn) - assert pyatmo.helpers.todayStamps() == expected + assert pyatmo.helpers.today_stamps() == expected diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 6e0dbf9f8..334bdd53e 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -1,4 +1,5 @@ """Define tests for Camera module.""" +# pylint: disable=protected-access import json import pytest @@ -9,10 +10,10 @@ INVALID_NAME = "InvalidName" -def test_CameraData(cameraHomeData): - assert cameraHomeData.default_home == "MYHOME" - assert cameraHomeData.default_camera["id"] == "12:34:56:00:f1:62" - assert cameraHomeData.default_camera["name"] == "Hall" +def test_camera_data(camera_home_data): + assert camera_home_data.default_home == "MYHOME" + assert camera_home_data.default_camera["id"] == "12:34:56:00:f1:62" + assert camera_home_data.default_camera["name"] == "Hall" @pytest.mark.parametrize( @@ -23,11 +24,11 @@ def test_CameraData(cameraHomeData): pytest.param(None, None), ], ) -def test_CameraData_homeById(cameraHomeData, hid, expected): +def test_camera_data_home_by_id(camera_home_data, hid, expected): if hid is None or hid == INVALID_NAME: - assert cameraHomeData.homeById(hid) is None + assert camera_home_data.home_by_id(hid) is None else: - assert cameraHomeData.homeById(hid)["name"] == expected + assert camera_home_data.home_by_id(hid)["name"] == expected @pytest.mark.parametrize( @@ -39,12 +40,12 @@ def test_CameraData_homeById(cameraHomeData, hid, expected): pytest.param(INVALID_NAME, None), ], ) -def test_CameraData_homeByName(cameraHomeData, name, expected): +def test_camera_data_home_by_name(camera_home_data, name, expected): if name == INVALID_NAME: with pytest.raises(pyatmo.exceptions.InvalidHome): - assert cameraHomeData.homeByName(name) + assert camera_home_data.home_by_name(name) else: - assert cameraHomeData.homeByName(name)["id"] == expected + assert camera_home_data.home_by_name(name)["id"] == expected @pytest.mark.parametrize( @@ -56,8 +57,8 @@ def test_CameraData_homeByName(cameraHomeData, name, expected): (None, None), ], ) -def test_CameraData_cameraById(cameraHomeData, cid, expected): - camera = cameraHomeData.cameraById(cid) +def test_camera_data_camera_by_id(camera_home_data, cid, expected): + camera = camera_home_data.camera_by_id(cid) if camera: assert camera["name"] == expected else: @@ -81,23 +82,23 @@ def test_CameraData_cameraById(cameraHomeData, cid, expected): (None, INVALID_NAME, None, None), ], ) -def test_CameraData_cameraByName(cameraHomeData, name, home, home_id, expected): +def test_camera_data_camera_by_name(camera_home_data, name, home, home_id, expected): if home == INVALID_NAME or name == INVALID_NAME or home_id == "InvalidHomeID": - assert cameraHomeData.cameraByName(name, home, home_id) is None + assert camera_home_data.camera_by_name(name, home, home_id) is None elif home_id is None: - assert cameraHomeData.cameraByName(name, home)["id"] == expected + assert camera_home_data.camera_by_name(name, home)["id"] == expected elif home is None: - assert cameraHomeData.cameraByName(name, home_id=home_id)["id"] == expected + assert camera_home_data.camera_by_name(name, home_id=home_id)["id"] == expected else: - assert cameraHomeData.cameraByName(name, home, home_id)["id"] == expected + assert camera_home_data.camera_by_name(name, home, home_id)["id"] == expected -def test_CameraData_moduleById(cameraHomeData): - assert cameraHomeData.moduleById("00:00:00:00:00:00") is None +def test_camera_data_module_by_id(camera_home_data): + assert camera_home_data.module_by_id("00:00:00:00:00:00") is None -def test_CameraData_moduleByName(cameraHomeData): - assert cameraHomeData.moduleByName() is None +def test_camera_data_module_by_name(camera_home_data): + assert camera_home_data.module_by_name() is None @pytest.mark.parametrize( @@ -114,44 +115,47 @@ def test_CameraData_moduleByName(cameraHomeData): pytest.param(None, INVALID_NAME, None, None), ], ) -def test_CameraData_cameraType(cameraHomeData, camera, home, cid, expected): - assert cameraHomeData.cameraType(camera, home, cid) == expected +def test_camera_data_camera_type(camera_home_data, camera, home, cid, expected): + assert camera_home_data.camera_type(camera, home, cid) == expected -def test_CameraData_cameraUrls(cameraHomeData, requests_mock): +def test_camera_data_camera_urls(camera_home_data, requests_mock): vpn_url = ( "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/" "6d278460699e56180d47ab47169efb31/" "MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTg,," ) local_url = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" - with open("fixtures/camera_ping.json") as f: - json_fixture = json.load(f) + with open("fixtures/camera_ping.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( vpn_url + "/command/ping", json=json_fixture, headers={"content-type": "application/json"}, ) - with open("fixtures/camera_ping.json") as f: - json_fixture = json.load(f) + with open("fixtures/camera_ping.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( local_url + "/command/ping", json=json_fixture, headers={"content-type": "application/json"}, ) - assert cameraHomeData.cameraUrls() == (vpn_url, local_url) + camera_id = "12:34:56:00:f1:62" + assert camera_home_data.camera_urls(camera_id) == (vpn_url, local_url) -def test_CameraData_cameraUrls_disconnected(auth, requests_mock): - with open("fixtures/camera_home_data_disconnected.json") as f: - json_fixture = json.load(f) + +def test_camera_data_camera_urls_disconnected(auth, requests_mock): + with open("fixtures/camera_home_data_disconnected.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.camera._GETHOMEDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) camera_data = pyatmo.CameraData(auth) - assert camera_data.cameraUrls() == (None, None) + camera_id = "12:34:56:00:f1:62" + assert camera_data.camera_urls(camera_id) == (None, None) @pytest.mark.parametrize( @@ -166,12 +170,12 @@ def test_CameraData_cameraUrls_disconnected(auth, requests_mock): ), ], ) -def test_CameraData_personsAtHome(cameraHomeData, home, expected): +def test_camera_data_persons_at_home(camera_home_data, home, expected): if home == INVALID_NAME: with pytest.raises(pyatmo.exceptions.InvalidHome): - assert cameraHomeData.personsAtHome(home) + assert camera_home_data.persons_at_home_by_name(home) else: - assert cameraHomeData.personsAtHome(home) == expected + assert camera_home_data.persons_at_home_by_name(home) == expected @freeze_time("2019-06-16") @@ -186,22 +190,26 @@ def test_CameraData_personsAtHome(cameraHomeData, home, expected): ("Jack Doe", None, False), ], ) -def test_CameraData_personSeenByCamera(cameraHomeData, name, exclude, expected): - assert cameraHomeData.personSeenByCamera(name, exclude=exclude) is expected +def test_camera_data_person_seen_by_camera(camera_home_data, name, exclude, expected): + camera_id = "12:34:56:00:f1:62" + assert ( + camera_home_data.person_seen_by_camera(name, camera_id, exclude=exclude) + is expected + ) -def test_CameraData__knownPersons(cameraHomeData): - knownPersons = cameraHomeData._knownPersons() - assert len(knownPersons) == 3 - assert knownPersons["91827374-7e04-5298-83ad-a0cb8372dff1"]["pseudo"] == "John Doe" +def test_camera_data__known_persons_dict(camera_home_data): + known_persons = camera_home_data._known_persons_dict() + assert len(known_persons) == 3 + assert known_persons["91827374-7e04-5298-83ad-a0cb8372dff1"]["pseudo"] == "John Doe" -def test_CameraData_knownPersonsNames(cameraHomeData): - assert sorted(cameraHomeData.knownPersonsNames()) == [ +def test_camera_data_known_persons_names(camera_home_data): + assert set(camera_home_data.known_persons_names()) == { "Jane Doe", "John Doe", "Richard Doe", - ] + } @freeze_time("2019-06-16") @@ -212,12 +220,12 @@ def test_CameraData_knownPersonsNames(cameraHomeData): ("Richard Doe", "91827376-7e04-5298-83af-a0cb8372dff3"), ], ) -def test_CameraData_getPersonId(cameraHomeData, name, expected): - assert cameraHomeData.getPersonId(name) == expected +def test_camera_data_get_person_id(camera_home_data, name, expected): + assert camera_home_data.get_person_id(name) == expected @pytest.mark.parametrize( - "hid, pid, json_fixture, expected", + "home_id, person_id, json_fixture, expected", [ ( "91763b24c43d3e344f424e8b", @@ -233,21 +241,21 @@ def test_CameraData_getPersonId(cameraHomeData, name, expected): ), ], ) -def test_CameraData_setPersonsAway( - cameraHomeData, requests_mock, hid, pid, json_fixture, expected +def test_camera_data_set_person_away( + camera_home_data, requests_mock, home_id, person_id, json_fixture, expected ): - with open("fixtures/%s" % json_fixture) as f: - json_fixture = json.load(f) + with open("fixtures/%s" % json_fixture) as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.camera._SETPERSONSAWAY_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - assert cameraHomeData.setPersonsAway(pid, hid)["status"] == expected + assert camera_home_data.set_person_away(person_id, home_id)["status"] == expected @pytest.mark.parametrize( - "hid, pids, json_fixture, expected", + "home_id, person_ids, json_fixture, expected", [ ( "91763b24c43d3e344f424e8b", @@ -266,78 +274,68 @@ def test_CameraData_setPersonsAway( ), ], ) -def test_CameraData_setPersonsHome( - cameraHomeData, requests_mock, hid, pids, json_fixture, expected +def test_camera_data_set_persons_home( + camera_home_data, requests_mock, home_id, person_ids, json_fixture, expected ): - with open("fixtures/%s" % json_fixture) as f: - json_fixture = json.load(f) + with open("fixtures/%s" % json_fixture) as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.camera._SETPERSONSHOME_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - assert cameraHomeData.setPersonsHome(pids, hid)["status"] == expected + assert camera_home_data.set_persons_home(person_ids, home_id)["status"] == expected @freeze_time("2019-06-16") @pytest.mark.parametrize( - "home, camera, exclude, expected", - [ - (None, None, None, True), - (None, None, 5, False), - (None, "InvalidCamera", None, False), - ("InvalidHome", None, None, False), - ], + "camera_id, exclude, expected", + [("12:34:56:00:f1:62", None, True), ("12:34:56:00:f1:62", 5, False)], ) -def test_CameraData_someoneKnownSeen(cameraHomeData, home, camera, exclude, expected): - assert cameraHomeData.someoneKnownSeen(home, camera, exclude) == expected +def test_camera_data_someone_known_seen(camera_home_data, camera_id, exclude, expected): + assert camera_home_data.someone_known_seen(camera_id, exclude) == expected @freeze_time("2019-06-16") @pytest.mark.parametrize( - "home, camera, exclude, expected", - [ - (None, None, None, False), - (None, None, 100, False), - (None, INVALID_NAME, None, False), - (INVALID_NAME, None, None, False), - ], + "camera_id, exclude, expected", + [("12:34:56:00:f1:62", None, False), ("12:34:56:00:f1:62", 100, False)], ) -def test_CameraData_someoneUnknownSeen(cameraHomeData, home, camera, exclude, expected): - assert cameraHomeData.someoneUnknownSeen(home, camera, exclude) == expected +def test_camera_data_someone_unknown_seen( + camera_home_data, camera_id, exclude, expected +): + assert camera_home_data.someone_unknown_seen(camera_id, exclude) == expected @freeze_time("2019-06-16") @pytest.mark.parametrize( - "home, camera, exclude, expected", + "camera_id, exclude, expected", [ - (None, None, None, False), - (None, None, 140000, True), - (None, None, 130000, False), - (None, INVALID_NAME, None, False), - (INVALID_NAME, None, None, False), + ("12:34:56:00:f1:62", None, False), + ("12:34:56:00:f1:62", 140000, True), + ("12:34:56:00:f1:62", 130000, False), ], ) -def test_CameraData_motionDetected(cameraHomeData, home, camera, exclude, expected): - assert cameraHomeData.motionDetected(home, camera, exclude) == expected +def test_camera_data_motion_detected(camera_home_data, camera_id, exclude, expected): + assert camera_home_data.motion_detected(camera_id, exclude) == expected -def test_CameraData_getHomeName(cameraHomeData): - assert cameraHomeData.getHomeName() == "MYHOME" +def test_camera_data_get_home_name(camera_home_data): + assert camera_home_data.get_home_name() == "MYHOME" home_id = "91763b24c43d3e344f424e8b" - assert cameraHomeData.getHomeName(home_id) == "MYHOME" + assert camera_home_data.get_home_name(home_id) == "MYHOME" home_id = "91763b24c43d3e344f424e8c" - assert cameraHomeData.getHomeName(home_id) == "Unknown" + assert camera_home_data.get_home_name(home_id) == "Unknown" home_id = "InvalidHomeID" with pytest.raises(pyatmo.InvalidHome): - assert cameraHomeData.getHomeName(home_id) == "Unknown" + assert camera_home_data.get_home_name(home_id) == "Unknown" -def test_CameraData_gethomeId(cameraHomeData): - assert cameraHomeData.gethomeId() == "91763b24c43d3e344f424e8b" - assert cameraHomeData.gethomeId("MYHOME") == "91763b24c43d3e344f424e8b" +def test_camera_data_get_home_id(camera_home_data): + assert camera_home_data.get_home_id() == "91763b24c43d3e344f424e8b" + assert camera_home_data.get_home_id("MYHOME") == "91763b24c43d3e344f424e8b" with pytest.raises(pyatmo.InvalidHome): - assert cameraHomeData.gethomeId("InvalidName") + assert camera_home_data.get_home_id("InvalidName") @pytest.mark.parametrize( @@ -349,8 +347,8 @@ def test_CameraData_gethomeId(cameraHomeData): (None, None), ], ) -def test_CameraData_smokedetectorById(cameraHomeData, sid, expected): - smokedetector = cameraHomeData.smokedetectorById(sid) +def test_camera_data_smokedetector_by_id(camera_home_data, sid, expected): + smokedetector = camera_home_data.smokedetector_by_id(sid) if smokedetector: assert smokedetector["name"] == expected else: @@ -373,21 +371,27 @@ def test_CameraData_smokedetectorById(cameraHomeData, sid, expected): (None, INVALID_NAME, None, None), ], ) -def test_CameraData_smokedetectorByName(cameraHomeData, name, home, home_id, expected): +def test_camera_data_smokedetector_by_name( + camera_home_data, name, home, home_id, expected +): if ( home == INVALID_NAME or name == INVALID_NAME or (name is None and home is None and home_id is None) ): - assert cameraHomeData.smokedetectorByName(name, home, home_id) is None + assert camera_home_data.smokedetector_by_name(name, home, home_id) is None elif home_id is None: - assert cameraHomeData.smokedetectorByName(name, home)["id"] == expected + assert camera_home_data.smokedetector_by_name(name, home)["id"] == expected elif home is None: assert ( - cameraHomeData.smokedetectorByName(name, home_id=home_id)["id"] == expected + camera_home_data.smokedetector_by_name(name, home_id=home_id)["id"] + == expected ) else: - assert cameraHomeData.smokedetectorByName(name, home, home_id)["id"] == expected + assert ( + camera_home_data.smokedetector_by_name(name, home, home_id)["id"] + == expected + ) @pytest.mark.parametrize( @@ -436,8 +440,8 @@ def test_CameraData_smokedetectorByName(cameraHomeData, name, home, home_id, exp ), ], ) -def test_CameraData_set_state( - cameraHomeData, +def test_camera_data_set_state( + camera_home_data, requests_mock, home_id, camera_id, @@ -454,7 +458,7 @@ def test_CameraData_set_state( headers={"content-type": "application/json"}, ) assert ( - cameraHomeData.set_state( + camera_home_data.set_state( home_id=home_id, camera_id=camera_id, floodlight=floodlight, diff --git a/tests/test_pyatmo_homecoach.py b/tests/test_pyatmo_homecoach.py index eb360385c..54b8d62f6 100644 --- a/tests/test_pyatmo_homecoach.py +++ b/tests/test_pyatmo_homecoach.py @@ -1,4 +1,5 @@ """Define tests for HomeCoach module.""" +# pylint: disable=protected-access import json import pytest @@ -6,8 +7,8 @@ import pyatmo -def test_HomeCoachData(homeCoachData): - assert homeCoachData.default_station == "Bedroom" +def test_home_coach_data(home_coach_data): + assert home_coach_data.default_station == "Bedroom" @pytest.mark.parametrize( @@ -24,8 +25,8 @@ def test_HomeCoachData(homeCoachData): ), ], ) -def test_HomeCoachData_modulesNamesList(homeCoachData, station, expected): - assert sorted(homeCoachData.modulesNamesList(station)) == expected +def test_home_coach_data_modules_names_list(home_coach_data, station, expected): + assert sorted(home_coach_data.modules_names_list(station)) == expected @pytest.mark.parametrize( @@ -80,13 +81,13 @@ def test_HomeCoachData_modulesNamesList(homeCoachData, station, expected): ), ], ) -def test_HomeCoachData_getModules(homeCoachData, station, expected): - assert homeCoachData.getModules(station) == expected +def test_home_coach_data_get_modules(home_coach_data, station, expected): + assert home_coach_data.get_modules(station) == expected -def test_HomeCoachData_no_devices(auth, requests_mock): - with open("fixtures/home_coach_no_devices.json") as f: - json_fixture = json.load(f) +def test_home_coach_data_no_devices(auth, requests_mock): + with open("fixtures/home_coach_no_devices.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.home_coach._GETHOMECOACHDATA_REQ, json=json_fixture, diff --git a/tests/test_pyatmo_publicdata.py b/tests/test_pyatmo_publicdata.py index d16b88c11..e86b17f83 100644 --- a/tests/test_pyatmo_publicdata.py +++ b/tests/test_pyatmo_publicdata.py @@ -1,4 +1,5 @@ """Define tests for Public weather module.""" +# pylint: disable=protected-access import json import pytest @@ -6,30 +7,30 @@ import pyatmo -def test_PublicData(auth, requests_mock): - with open("fixtures/public_data_simple.json") as f: - json_fixture = json.load(f) +def test_public_data(auth, requests_mock): + with open("fixtures/public_data_simple.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.public_data._GETPUBLIC_DATA, json=json_fixture, headers={"content-type": "application/json"}, ) - publicData = pyatmo.PublicData(auth) - assert publicData.status == "ok" + public_data = pyatmo.PublicData(auth) + assert public_data.status == "ok" - publicData = pyatmo.PublicData(auth, required_data_type="temperature,rain_live") - assert publicData.status == "ok" + public_data = pyatmo.PublicData(auth, required_data_type="temperature,rain_live") + assert public_data.status == "ok" -def test_PublicData_unavailable(auth, requests_mock): +def test_public_data_unavailable(auth, requests_mock): requests_mock.post(pyatmo.public_data._GETPUBLIC_DATA, status_code=404) with pytest.raises(pyatmo.ApiError): pyatmo.PublicData(auth) -def test_PublicData_error(auth, requests_mock): - with open("fixtures/public_data_error_mongo.json") as f: - json_fixture = json.load(f) +def test_public_data_error(auth, requests_mock): + with open("fixtures/public_data_error_mongo.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.public_data._GETPUBLIC_DATA, json=json_fixture, @@ -39,56 +40,53 @@ def test_PublicData_error(auth, requests_mock): pyatmo.PublicData(auth) -def test_PublicData_CountStationInArea(publicData): - assert publicData.CountStationInArea() == 8 +def test_public_data_count_station_in_area(public_data): + assert public_data.count_station_in_area() == 8 -def test_PublicData_getLatestRain(publicData): +def test_public_data_get_latest_rain(public_data): expected = { "70:ee:50:1f:68:9e": 0, "70:ee:50:27:25:b0": 0, "70:ee:50:36:94:7c": 0.5, "70:ee:50:36:a9:fc": 0, } - assert publicData.getLatestRain() == expected - assert publicData.getLive() == expected + assert public_data.get_latest_rain() == expected -def test_PublicData_getAverageRain(publicData): - assert publicData.getAverageRain() == 0.125 +def test_public_data_get_average_rain(public_data): + assert public_data.get_average_rain() == 0.125 -def test_PublicData_get60minRain(publicData): +def test_public_data_get_60min_rain(public_data): expected = { "70:ee:50:1f:68:9e": 0, "70:ee:50:27:25:b0": 0, "70:ee:50:36:94:7c": 0.2, "70:ee:50:36:a9:fc": 0, } - assert publicData.get60min() == expected - assert publicData.get60minRain() == expected + assert public_data.get_60min_rain() == expected -def test_PublicData_getAverage60minRain(publicData): - assert publicData.getAverage60minRain() == 0.05 +def test_public_data_get_average_60min_rain(public_data): + assert public_data.get_average_60min_rain() == 0.05 -def test_PublicData_get24hRain(publicData): +def test_public_data_get_24h_rain(public_data): expected = { "70:ee:50:1f:68:9e": 9.999, "70:ee:50:27:25:b0": 11.716000000000001, "70:ee:50:36:94:7c": 12.322000000000001, "70:ee:50:36:a9:fc": 11.009, } - assert publicData.get24h() == expected - assert publicData.get24hRain() == expected + assert public_data.get_24h_rain() == expected -def test_PublicData_getAverage24hRain(publicData): - assert publicData.getAverage24hRain() == 11.261500000000002 +def test_public_data_get_average_24h_rain(public_data): + assert public_data.get_average_24h_rain() == 11.261500000000002 -def test_PublicData_getLatestPressures(publicData): +def test_public_data_get_latest_pressures(public_data): expected = { "70:ee:50:1f:68:9e": 1007.3, "70:ee:50:27:25:b0": 1012.8, @@ -99,14 +97,14 @@ def test_PublicData_getLatestPressures(publicData): "70:ee:50:27:9f:2c": 1010.6, "70:ee:50:3c:02:78": 1011.7, } - assert publicData.getLatestPressures() == expected + assert public_data.get_latest_pressures() == expected -def test_PublicData_getAveragePressure(publicData): - assert publicData.getAveragePressure() == 1010.3499999999999 +def test_public_data_get_average_pressure(public_data): + assert public_data.get_average_pressure() == 1010.3499999999999 -def test_PublicData_getLatestTemperatures(publicData): +def test_public_data_get_latest_temperatures(public_data): expected = { "70:ee:50:1f:68:9e": 21.1, "70:ee:50:27:25:b0": 23.2, @@ -117,14 +115,14 @@ def test_PublicData_getLatestTemperatures(publicData): "70:ee:50:27:9f:2c": 25.5, "70:ee:50:3c:02:78": 23.3, } - assert publicData.getLatestTemperatures() == expected + assert public_data.get_latest_temperatures() == expected -def test_PublicData_getAverageTemperature(publicData): - assert publicData.getAverageTemperature() == 22.725 +def test_public_data_get_average_temperature(public_data): + assert public_data.get_average_temperature() == 22.725 -def test_PublicData_getLatestHumidities(publicData): +def test_public_data_get_latest_humidities(public_data): expected = { "70:ee:50:1f:68:9e": 69, "70:ee:50:27:25:b0": 60, @@ -135,42 +133,42 @@ def test_PublicData_getLatestHumidities(publicData): "70:ee:50:27:9f:2c": 56, "70:ee:50:3c:02:78": 58, } - assert publicData.getLatestHumidities() == expected + assert public_data.get_latest_humidities() == expected -def test_PublicData_getAverageHumidity(publicData): - assert publicData.getAverageHumidity() == 63.25 +def test_public_data_get_average_humidity(public_data): + assert public_data.get_average_humidity() == 63.25 -def test_PublicData_getLatestWindStrengths(publicData): +def test_public_data_get_latest_wind_strengths(public_data): expected = {"70:ee:50:36:a9:fc": 15} - assert publicData.getLatestWindStrengths() == expected + assert public_data.get_latest_wind_strengths() == expected -def test_PublicData_getAverageWindStrength(publicData): - assert publicData.getAverageWindStrength() == 15 +def test_public_data_get_average_wind_strength(public_data): + assert public_data.get_average_wind_strength() == 15 -def test_PublicData_getLatestWindAngles(publicData): +def test_public_data_get_latest_wind_angles(public_data): expected = {"70:ee:50:36:a9:fc": 17} - assert publicData.getLatestWindAngles() == expected + assert public_data.get_latest_wind_angles() == expected -def test_PublicData_getLatestGustStrengths(publicData): +def test_public_data_get_latest_gust_strengths(public_data): expected = {"70:ee:50:36:a9:fc": 31} - assert publicData.getLatestGustStrengths() == expected + assert public_data.get_latest_gust_strengths() == expected -def test_PublicData_getAverageGustStrength(publicData): - assert publicData.getAverageGustStrength() == 31 +def test_public_data_get_average_gust_strength(public_data): + assert public_data.get_average_gust_strength() == 31 -def test_PublicData_getLatestGustAngles(publicData): +def test_public_data_get_latest_gust_angles(public_data): expected = {"70:ee:50:36:a9:fc": 217} - assert publicData.getLatestGustAngles() == expected + assert public_data.get_latest_gust_angles() == expected -def test_PublicData_getLocations(publicData): +def test_public_data_get_locations(public_data): expected = { "70:ee:50:1f:68:9e": [8.795445200000017, 50.2130169], "70:ee:50:27:25:b0": [8.7807159, 50.1946167], @@ -181,23 +179,22 @@ def test_PublicData_getLocations(publicData): "70:ee:50:27:9f:2c": [8.785342, 50.193573], "70:ee:50:3c:02:78": [8.795953681700666, 50.19530139868166], } - assert publicData.getLocations() == expected + assert public_data.get_locations() == expected -def test_PublicData_getTimeforMeasure(publicData): +def test_public_data_get_time_for_measures(public_data): expected = { "70:ee:50:36:a9:fc": 1560248184, "70:ee:50:1f:68:9e": 1560248344, "70:ee:50:27:25:b0": 1560247896, "70:ee:50:36:94:7c": 1560248022, } - assert publicData.getTimeforMeasure() == expected - assert publicData.getTimeForRainMeasures() == expected + assert public_data.get_time_for_rain_measures() == expected -def test_PublicData_getTimeForWindMeasures(publicData): +def test_public_data_get_time_for_wind_measures(public_data): expected = {"70:ee:50:36:a9:fc": 1560248190} - assert publicData.getTimeForWindMeasures() == expected + assert public_data.get_time_for_wind_measures() == expected @pytest.mark.parametrize( @@ -244,8 +241,8 @@ def test_PublicData_getTimeForWindMeasures(publicData): ), ], ) -def test_PublicData_getLatestStationMeasures(publicData, test_input, expected): - assert publicData.getLatestStationMeasures(test_input) == expected +def test_public_data_get_latest_station_measures(public_data, test_input, expected): + assert public_data.get_latest_station_measures(test_input) == expected @pytest.mark.parametrize( @@ -258,8 +255,8 @@ def test_PublicData_getLatestStationMeasures(publicData, test_input, expected): ("wind_timeutc", {"70:ee:50:36:a9:fc": 1560248190}), ], ) -def test_PublicData_getAccessoryMeasures(publicData, test_input, expected): - assert publicData.getAccessoryMeasures(test_input) == expected +def test_public_data_get_accessory_measures(public_data, test_input, expected): + assert public_data.get_accessory_measures(test_input) == expected @pytest.mark.parametrize( @@ -294,5 +291,5 @@ def test_PublicData_getAccessoryMeasures(publicData, test_input, expected): ({}, 0), ], ) -def test_PublicData_averageMeasure(test_input, expected): - assert pyatmo.public_data.averageMeasure(test_input) == expected +def test_public_data_average_measure(test_input, expected): + assert pyatmo.public_data.average_measure(test_input) == expected diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index 7c40a8afe..cacbfb10f 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -1,19 +1,20 @@ """Define tests for Thermostat module.""" +# pylint: disable=protected-access import json import pytest import pyatmo -from .conftest import does_not_raise +from tests.conftest import does_not_raise -def test_HomeData(homeData): - assert homeData.default_home == "MYHOME" - assert homeData.default_home_id == "91763b24c43d3e344f424e8b" - assert len(homeData.rooms[homeData.default_home_id]) == 4 +def test_home_data(home_data): + assert home_data.default_home == "MYHOME" + assert home_data.default_home_id == "91763b24c43d3e344f424e8b" + assert len(home_data.rooms[home_data.default_home_id]) == 4 - assert len(homeData.modules[homeData.default_home_id]) == 5 + assert len(home_data.modules[home_data.default_home_id]) == 5 expected = { "12:34:56:00:fa:d0": { @@ -59,18 +60,18 @@ def test_HomeData(homeData): "room_id": "3688132631", }, } - assert homeData.modules[homeData.default_home_id] == expected + assert home_data.modules[home_data.default_home_id] == expected -def test_HomeData_no_data(auth, requests_mock): +def test_home_data_no_data(auth, requests_mock): requests_mock.post(pyatmo.thermostat._GETHOMESDATA_REQ, text="None") with pytest.raises(pyatmo.NoDevice): assert pyatmo.HomeData(auth) -def test_HomeData_no_body(auth, requests_mock): - with open("fixtures/home_data_empty.json") as f: - json_fixture = json.load(f) +def test_home_data_no_body(auth, requests_mock): + with open("fixtures/home_data_empty.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, @@ -80,51 +81,51 @@ def test_HomeData_no_body(auth, requests_mock): assert pyatmo.HomeData(auth) -def test_HomeData_no_home_name(auth, requests_mock): - with open("fixtures/home_data_nohomename.json") as f: - json_fixture = json.load(f) +def test_home_data_no_home_name(auth, requests_mock): + with open("fixtures/home_data_nohomename.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - homeData = pyatmo.HomeData(auth) + home_data = pyatmo.HomeData(auth) home_id = "91763b24c43d3e344f424e8b" - assert homeData.homeById(home_id)["name"] == "Unknown" + assert home_data.home_by_id(home_id)["name"] == "Unknown" -def test_HomeData_homeById(homeData): +def test_home_data_home_by_id(home_data): home_id = "91763b24c43d3e344f424e8b" - assert homeData.homeById(home_id)["name"] == "MYHOME" + assert home_data.home_by_id(home_id)["name"] == "MYHOME" home_id = "91763b24c43d3e344f424e8c" - assert homeData.homeById(home_id)["name"] == "Unknown" + assert home_data.home_by_id(home_id)["name"] == "Unknown" -def test_HomeData_homeByName(homeData): - assert homeData.homeByName()["name"] == "MYHOME" - assert homeData.homeByName()["id"] == "91763b24c43d3e344f424e8b" +def test_home_data_home_by_name(home_data): + assert home_data.home_by_name()["name"] == "MYHOME" + assert home_data.home_by_name()["id"] == "91763b24c43d3e344f424e8b" -def test_HomeData_gethomeId(homeData): - assert homeData.gethomeId() == "91763b24c43d3e344f424e8b" - assert homeData.gethomeId("MYHOME") == "91763b24c43d3e344f424e8b" +def test_home_data_get_home_id(home_data): + assert home_data.get_home_id() == "91763b24c43d3e344f424e8b" + assert home_data.get_home_id("MYHOME") == "91763b24c43d3e344f424e8b" with pytest.raises(pyatmo.InvalidHome): - assert homeData.gethomeId("InvalidName") + assert home_data.get_home_id("InvalidName") -def test_HomeData_getHomeName(homeData): - assert homeData.getHomeName() == "MYHOME" +def test_home_data_get_home_name(home_data): + assert home_data.get_home_name() == "MYHOME" home_id = "91763b24c43d3e344f424e8b" - assert homeData.getHomeName(home_id) == "MYHOME" + assert home_data.get_home_name(home_id) == "MYHOME" home_id = "91763b24c43d3e344f424e8c" - assert homeData.getHomeName(home_id) == "Unknown" + assert home_data.get_home_name(home_id) == "Unknown" -def test_HomeData_getSelectedschedule(homeData): - assert homeData.getSelectedschedule()["name"] == "Default" - assert homeData.getSelectedschedule("MYHOME")["name"] == "Default" +def test_home_data_get_selected_schedule(home_data): + assert home_data.get_selected_schedule()["name"] == "Default" + assert home_data.get_selected_schedule("MYHOME")["name"] == "Default" with pytest.raises(pyatmo.InvalidHome): - assert homeData.getSelectedschedule("Unknown") + assert home_data.get_selected_schedule("Unknown") @pytest.mark.parametrize( @@ -137,25 +138,25 @@ def test_HomeData_getSelectedschedule(homeData): (None, "123456789abcdefg12345678", None, pytest.raises(pyatmo.NoSchedule)), ], ) -def test_HomeData_switchHomeSchedule( - homeData, requests_mock, t_home, t_sched_id, t_sched, expected +def test_home_data_switch_home_schedule( + home_data, requests_mock, t_home, t_sched_id, t_sched, expected ): - with open("fixtures/status_ok.json") as f: - json_fixture = json.load(f) + with open("fixtures/status_ok.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._SWITCHHOMESCHEDULE_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) with expected: - homeData.switchHomeSchedule( + home_data.switch_home_schedule( schedule_id=t_sched_id, schedule=t_sched, home=t_home ) -def test_HomeStatus(homeStatus): - assert len(homeStatus.rooms) == 3 - assert homeStatus.default_room["id"] == "2746182631" +def test_home_status(home_status): + assert len(home_status.rooms) == 3 + assert home_status.default_room["id"] == "2746182631" expexted = { "id": "2746182631", @@ -166,27 +167,27 @@ def test_HomeStatus(homeStatus): "therm_setpoint_start_time": 1559229567, "therm_setpoint_end_time": 0, } - assert homeStatus.default_room == expexted + assert home_status.default_room == expexted -def test_HomeStatus_error_and_data(auth, requests_mock): - with open("fixtures/home_status_error_and_data.json") as f: - json_fixture = json.load(f) +def test_home_status_error_and_data(auth, requests_mock): + with open("fixtures/home_status_error_and_data.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESTATUS_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with open("fixtures/home_data_simple.json") as f: - json_fixture = json.load(f) + with open("fixtures/home_data_simple.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - homeStatus = pyatmo.HomeStatus(auth) - assert len(homeStatus.rooms) == 3 - assert homeStatus.default_room["id"] == "2746182631" + home_status = pyatmo.HomeStatus(auth) + assert len(home_status.rooms) == 3 + assert home_status.default_room["id"] == "2746182631" expexted = { "id": "2746182631", @@ -197,19 +198,19 @@ def test_HomeStatus_error_and_data(auth, requests_mock): "therm_setpoint_start_time": 1559229567, "therm_setpoint_end_time": 0, } - assert homeStatus.default_room == expexted + assert home_status.default_room == expexted -def test_HomeStatus_error(auth, requests_mock): - with open("fixtures/home_status_empty.json") as f: - json_fixture = json.load(f) +def test_home_status_error(auth, requests_mock): + with open("fixtures/home_status_empty.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESTATUS_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with open("fixtures/home_data_simple.json") as f: - json_fixture = json.load(f) + with open("fixtures/home_data_simple.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, @@ -219,7 +220,7 @@ def test_HomeStatus_error(auth, requests_mock): assert pyatmo.HomeStatus(auth) -def test_HomeStatus_roomById(homeStatus): +def test_home_status_room_by_id(home_status): expexted = { "id": "2746182631", "reachable": True, @@ -229,12 +230,12 @@ def test_HomeStatus_roomById(homeStatus): "therm_setpoint_start_time": 1559229567, "therm_setpoint_end_time": 0, } - assert homeStatus.roomById("2746182631") == expexted + assert home_status.room_by_id("2746182631") == expexted with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.roomById("0000000000") + assert home_status.room_by_id("0000000000") -def test_HomeStatus_thermostatById(homeStatus): +def test_home_status_thermostat_by_id(home_status): expexted = { "id": "12:34:56:00:01:ae", "reachable": True, @@ -248,12 +249,12 @@ def test_HomeStatus_thermostatById(homeStatus): "bridge": "12:34:56:00:fa:d0", "battery_state": "high", } - assert homeStatus.thermostatById("12:34:56:00:01:ae") == expexted + assert home_status.thermostat_by_id("12:34:56:00:01:ae") == expexted with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.thermostatById("00:00:00:00:00:00") + assert home_status.thermostat_by_id("00:00:00:00:00:00") -def test_HomeStatus_relayById(homeStatus): +def test_home_status_relay_by_id(home_status): expexted = { "id": "12:34:56:00:fa:d0", "type": "NAPlug", @@ -261,12 +262,12 @@ def test_HomeStatus_relayById(homeStatus): "rf_strength": 107, "wifi_strength": 42, } - assert homeStatus.relayById("12:34:56:00:fa:d0") == expexted + assert home_status.relay_by_id("12:34:56:00:fa:d0") == expexted with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.relayById("00:00:00:00:00:00") + assert home_status.relay_by_id("00:00:00:00:00:00") -def test_HomeStatus_valveById(homeStatus): +def test_home_status_valve_by_id(home_status): expexted = { "id": "12:34:56:03:a5:54", "reachable": True, @@ -277,58 +278,58 @@ def test_HomeStatus_valveById(homeStatus): "bridge": "12:34:56:00:fa:d0", "battery_state": "full", } - assert homeStatus.valveById("12:34:56:03:a5:54") == expexted + assert home_status.valve_by_id("12:34:56:03:a5:54") == expexted with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.valveById("00:00:00:00:00:00") + assert home_status.valve_by_id("00:00:00:00:00:00") -def test_HomeStatus_setPoint(homeStatus): - assert homeStatus.setPoint() == 12 - assert homeStatus.setPoint("2746182631") == 12 +def test_home_status_set_point(home_status): + assert home_status.set_point() == 12 + assert home_status.set_point("2746182631") == 12 with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.setPoint("0000000000") + assert home_status.set_point("0000000000") -def test_HomeStatus_setPointmode(homeStatus): - assert homeStatus.setPointmode() == "away" - assert homeStatus.setPointmode("2746182631") == "away" - assert homeStatus.setPointmode("0000000000") is None +def test_home_status_set_point_mode(home_status): + assert home_status.set_point_mode() == "away" + assert home_status.set_point_mode("2746182631") == "away" + assert home_status.set_point_mode("0000000000") is None -def test_HomeStatus_getAwaytemp(homeStatus): - assert homeStatus.getAwaytemp() == 14 - assert homeStatus.getAwaytemp("MYHOME") == 14 - assert homeStatus.getAwaytemp("InvalidName") is None - assert homeStatus.getAwaytemp(home_id="91763b24c43d3e344f424e8b") == 14 - assert homeStatus.getAwaytemp(home_id="00000000000000000000000") is None +def test_home_status_get_away_temp(home_status): + assert home_status.get_away_temp() == 14 + assert home_status.get_away_temp("MYHOME") == 14 + assert home_status.get_away_temp("InvalidName") is None + assert home_status.get_away_temp(home_id="91763b24c43d3e344f424e8b") == 14 + assert home_status.get_away_temp(home_id="00000000000000000000000") is None -def test_HomeStatus_getHgtemp(homeStatus): - assert homeStatus.getHgtemp() == 7 - assert homeStatus.getHgtemp("MYHOME") == 7 +def test_home_status_get_hg_temp(home_status): + assert home_status.get_hg_temp() == 7 + assert home_status.get_hg_temp("MYHOME") == 7 with pytest.raises(pyatmo.InvalidHome): - assert homeStatus.getHgtemp("InvalidHome") - assert homeStatus.getHgtemp(home_id="91763b24c43d3e344f424e8b") == 7 - assert homeStatus.getHgtemp(home_id="00000000000000000000000") is None + assert home_status.get_hg_temp("InvalidHome") + assert home_status.get_hg_temp(home_id="91763b24c43d3e344f424e8b") == 7 + assert home_status.get_hg_temp(home_id="00000000000000000000000") is None -def test_HomeStatus_measuredTemperature(homeStatus): - assert homeStatus.measuredTemperature() == 19.8 - assert homeStatus.measuredTemperature("2746182631") == 19.8 +def test_home_status_measured_temperature(home_status): + assert home_status.measured_temperature() == 19.8 + assert home_status.measured_temperature("2746182631") == 19.8 with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.measuredTemperature("0000000000") + assert home_status.measured_temperature("0000000000") -def test_HomeStatus_boilerStatus(homeStatus): - assert homeStatus.boilerStatus() is False +def test_home_status_boiler_status(home_status): + assert home_status.boiler_status() is False -def test_HomeStatus_thermostatType(homeStatus): - assert homeStatus.thermostatType("MYHOME", "2746182631") == "NATherm1" - assert homeStatus.thermostatType("MYHOME", "2833524037") == "NRV" +def test_home_status_thermostat_type(home_status): + assert home_status.thermostat_type("MYHOME", "2746182631") == "NATherm1" + assert home_status.thermostat_type("MYHOME", "2833524037") == "NRV" with pytest.raises(pyatmo.InvalidHome): - assert homeStatus.thermostatType("InvalidHome", "2833524037") - assert homeStatus.thermostatType("MYHOME", "0000000000") is None + assert home_status.thermostat_type("InvalidHome", "2833524037") + assert home_status.thermostat_type("MYHOME", "0000000000") is None @pytest.mark.parametrize( @@ -394,8 +395,8 @@ def test_HomeStatus_thermostatType(homeStatus): ), ], ) -def test_HomeData_setThermmode( - homeStatus, +def test_home_data_set_therm_mode( + home_status, requests_mock, caplog, home_id, @@ -405,14 +406,14 @@ def test_HomeData_setThermmode( json_fixture, expected, ): - with open("fixtures/%s" % json_fixture) as f: - json_fixture = json.load(f) + with open("fixtures/%s" % json_fixture) as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._SETTHERMMODE_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - res = homeStatus.setThermmode( + res = home_status.set_therm_mode( home_id=home_id, mode=mode, end_time=end_time, schedule_id=schedule_id ) if "error" in res: @@ -462,8 +463,8 @@ def test_HomeData_setThermmode( ), ], ) -def test_HomeData_setroomThermpoint( - homeStatus, +def test_home_data_set_room_therm_point( + home_status, requests_mock, caplog, home_id, @@ -474,15 +475,15 @@ def test_HomeData_setroomThermpoint( json_fixture, expected, ): - with open("fixtures/%s" % json_fixture) as f: - json_fixture = json.load(f) + with open("fixtures/%s" % json_fixture) as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._SETROOMTHERMPOINT_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) assert ( - homeStatus.setroomThermpoint( + home_status.set_room_therm_point( home_id=home_id, room_id=room_id, mode=mode, temp=temp, end_time=end_time )["status"] == expected @@ -526,42 +527,34 @@ def test_HomeData_setroomThermpoint( ), ], ) -def test_HomeData_setroomThermpoint_error( - homeStatus, - requests_mock, - caplog, - home_id, - room_id, - mode, - temp, - json_fixture, - expected, +def test_home_data_set_room_therm_point_error( + home_status, requests_mock, home_id, room_id, mode, temp, json_fixture, expected ): - with open("fixtures/%s" % json_fixture) as f: - json_fixture = json.load(f) + with open("fixtures/%s" % json_fixture) as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._SETROOMTHERMPOINT_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) assert ( - homeStatus.setroomThermpoint( + home_status.set_room_therm_point( home_id=home_id, room_id=room_id, mode=mode, temp=temp )["error"]["message"] == expected ) -def test_HomeStatus_error_disconnected(auth, requests_mock): - with open("fixtures/home_status_error_disconnected.json") as f: - json_fixture = json.load(f) +def test_home_status_error_disconnected(auth, requests_mock): + with open("fixtures/home_status_error_disconnected.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESTATUS_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) - with open("fixtures/home_data_simple.json") as f: - json_fixture = json.load(f) + with open("fixtures/home_data_simple.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 87ea4e178..7abaee614 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -1,4 +1,5 @@ """Define tests for WeatherStation module.""" +# pylint: disable=protected-access import json import pytest @@ -7,19 +8,19 @@ import pyatmo -def test_WeatherStationData(weatherStationData): - assert weatherStationData.default_station == "MyStation" +def test_weather_station_data(weather_station_data): + assert weather_station_data.default_station == "MyStation" -def test_WeatherStationData_no_response(auth, requests_mock): +def test_weather_station_data_no_response(auth, requests_mock): requests_mock.post(pyatmo.weather_station._GETSTATIONDATA_REQ, text="None") with pytest.raises(pyatmo.NoDevice): assert pyatmo.WeatherStationData(auth) -def test_WeatherStationData_no_body(auth, requests_mock): - with open("fixtures/status_ok.json") as f: - json_fixture = json.load(f) +def test_weather_station_data_no_body(auth, requests_mock): + with open("fixtures/status_ok.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.weather_station._GETSTATIONDATA_REQ, json=json_fixture, @@ -29,9 +30,9 @@ def test_WeatherStationData_no_body(auth, requests_mock): assert pyatmo.WeatherStationData(auth) -def test_WeatherStationData_no_data(auth, requests_mock): - with open("fixtures/home_data_empty.json") as f: - json_fixture = json.load(f) +def test_weather_station_data_no_data(auth, requests_mock): + with open("fixtures/home_data_empty.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.weather_station._GETSTATIONDATA_REQ, json=json_fixture, @@ -84,8 +85,10 @@ def test_WeatherStationData_no_data(auth, requests_mock): ), ], ) -def test_WeatherStationData_modulesNamesList(weatherStationData, station, expected): - assert sorted(weatherStationData.modulesNamesList(station)) == expected +def test_weather_station_data_modules_names_list( + weather_station_data, station, expected +): + assert sorted(weather_station_data.modules_names_list(station)) == expected @pytest.mark.parametrize( @@ -210,12 +213,12 @@ def test_WeatherStationData_modulesNamesList(weatherStationData, station, expect ), ], ) -def test_WeatherStationData_getModules(weatherStationData, station, expected): - assert weatherStationData.getModules(station) == expected +def test_weather_station_data_get_modules(weather_station_data, station, expected): + assert weather_station_data.get_modules(station) == expected -def test_WeatherStationData_stationByName(weatherStationData): - result = weatherStationData.stationByName() +def test_weather_station_data_station_by_name(weather_station_data): + result = weather_station_data.station_by_name() assert result["_id"] == "12:34:56:37:11:ca" assert result["station_name"] == "MyStation" assert result["module_name"] == "NetatmoIndoor" @@ -227,7 +230,7 @@ def test_WeatherStationData_stationByName(weatherStationData): "Noise", "Pressure", ] - assert weatherStationData.stationByName("NoValidStation") is None + assert weather_station_data.station_by_name("NoValidStation") is None @pytest.mark.parametrize( @@ -243,8 +246,10 @@ def test_WeatherStationData_stationByName(weatherStationData): (None, None, None), ], ) -def test_WeatherStationData_moduleByName(weatherStationData, module, station, expected): - mod = weatherStationData.moduleByName(module, station) +def test_weather_station_data_module_by_name( + weather_station_data, module, station, expected +): + mod = weather_station_data.module_by_name(module, station) if mod: assert mod["_id"] == expected else: @@ -261,8 +266,8 @@ def test_WeatherStationData_moduleByName(weatherStationData, module, station, ex (None, None, None), ], ) -def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): - mod = weatherStationData.moduleById(mid, sid) +def test_weather_station_data_module_by_id(weather_station_data, mid, sid, expected): + mod = weather_station_data.module_by_id(mid, sid) if mod: assert mod["_id"] == expected else: @@ -270,7 +275,7 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): @pytest.mark.parametrize( - "module, moduleId, expected", + "module, module_id, expected", [ ( None, @@ -364,18 +369,22 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): ), ], ) -def test_WeatherStationData_monitoredConditions( - weatherStationData, module, moduleId, expected +def test_weather_station_data_monitored_conditions( + weather_station_data, module, module_id, expected ): assert ( - sorted(weatherStationData.monitoredConditions(module=module, moduleId=moduleId)) + sorted( + weather_station_data.monitored_conditions( + module=module, module_id=module_id + ) + ) == expected ) @freeze_time("2019-06-11") @pytest.mark.parametrize( - "station, exclude, byId, expected", + "station, exclude, by_id, expected", [ ( "MyStation", @@ -462,10 +471,10 @@ def test_WeatherStationData_monitoredConditions( ), ], ) -def test_WeatherStationData_lastData( - weatherStationData, station, exclude, byId, expected +def test_weather_station_data_last_data( + weather_station_data, station, exclude, by_id, expected ): - mod = weatherStationData.lastData(station=station, exclude=exclude, byId=byId) + mod = weather_station_data.last_data(station=station, exclude=exclude, by_id=by_id) if mod: assert sorted(mod) == expected else: @@ -488,8 +497,8 @@ def test_WeatherStationData_lastData( "12:34:56:37:11:ca", ], ), - ("", None, {},), - ("NoValidStation", None, {},), + ("", None, {}), + ("NoValidStation", None, {}), ( "12:34:56:37:11:ca", 1000000, @@ -515,10 +524,10 @@ def test_WeatherStationData_lastData( ), ], ) -def test_WeatherStationData_lastData_byId( - weatherStationData, station, exclude, expected +def test_weather_station_data_last_data_by_id( + weather_station_data, station, exclude, expected ): - mod = weatherStationData.lastData(station, exclude, byId=True) + mod = weather_station_data.last_data(station, exclude, by_id=True) if mod: assert sorted(mod) == expected else: @@ -573,10 +582,10 @@ def test_WeatherStationData_lastData_byId( ), ], ) -def test_WeatherStationData_checkNotUpdated( - weatherStationData, station, delay, expected +def test_weather_station_data_check_not_updated( + weather_station_data, station, delay, expected ): - mod = weatherStationData.checkNotUpdated(station, delay) + mod = weather_station_data.check_not_updated(station, delay) assert sorted(mod) == expected @@ -616,8 +625,10 @@ def test_WeatherStationData_checkNotUpdated( ), ], ) -def test_WeatherStationData_checkUpdated(weatherStationData, station, delay, expected): - mod = weatherStationData.checkUpdated(station, delay) +def test_weather_station_data_check_updated( + weather_station_data, station, delay, expected +): + mod = weather_station_data.check_updated(station, delay) assert sorted(mod) == expected @@ -625,24 +636,24 @@ def test_WeatherStationData_checkUpdated(weatherStationData, station, delay, exp @pytest.mark.parametrize( "device_id, scale, mtype, expected", [("MyStation", "scale", "type", [28.1])] ) -def test_WeatherStationData_getMeasure( - weatherStationData, requests_mock, device_id, scale, mtype, expected +def test_weather_station_data_get_measure( + weather_station_data, requests_mock, device_id, scale, mtype, expected ): - with open("fixtures/weatherstation_measure.json") as f: - json_fixture = json.load(f) + with open("fixtures/weatherstation_measure.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.weather_station._GETMEASURE_REQ, json=json_fixture, headers={"content-type": "application/json"}, ) assert ( - weatherStationData.getMeasure(device_id, scale, mtype)["body"]["1544558433"] + weather_station_data.get_measure(device_id, scale, mtype)["body"]["1544558433"] == expected ) -def test_WeatherStationData_lastData_measurements(weatherStationData): - mod = weatherStationData.lastData("MyStation", None) +def test_weather_station_data_last_data_measurements(weather_station_data): + mod = weather_station_data.last_data("MyStation", None) assert mod["NetatmoIndoor"]["min_temp"] == 23.4 assert mod["NetatmoIndoor"]["max_temp"] == 25.6 assert mod["NetatmoIndoor"]["Temperature"] == 24.6 @@ -688,13 +699,13 @@ def test_WeatherStationData_lastData_measurements(weatherStationData): "12:34:56:37:11:ca", ], ), - ("12:34:56:00:aa:01", None, {},), + ("12:34:56:00:aa:01", None, {}), ], ) -def test_WeatherStationData_lastData_bug_97( - weatherStationData, station, exclude, expected +def test_weather_station_data_last_data_bug_97( + weather_station_data, station, exclude, expected ): - mod = weatherStationData.lastData(station, exclude, byId=True) + mod = weather_station_data.last_data(station, exclude, by_id=True) if mod: assert sorted(mod) == expected else: diff --git a/tox.ini b/tox.ini index d86079d3c..e58ac6d89 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ deps = requests-mock freezegun -commands = +commands = python -m pytest --cov {envsitepackagesdir}/pyatmo [coverage:paths] diff --git a/usage.md b/usage.md index fc286bd24..540179152 100644 --- a/usage.md +++ b/usage.md @@ -79,8 +79,8 @@ CLIENT_SECRET = '123456789abcd1234' USERNAME = 'your@account.com' PASSWORD = 'abcdef-123456-ghijkl' authorization = pyatmo.ClientAuth( - clientId=CLIENT_ID, - clientSecret=CLIENT_SECRET, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, username=USERNAME, password=PASSWORD, ) @@ -92,8 +92,8 @@ weatherData = pyatmo.WeatherStationData(authorization) print( "Current temperature (inside/outside): %s / %s °C" % ( - weatherData.lastData()["indoor"]["Temperature"], - weatherData.lastData()["outdoor"]["Temperature"], + weatherData.last_data()["indoor"]["Temperature"], + weatherData.last_data()["outdoor"]["Temperature"], ) ) ``` @@ -104,7 +104,7 @@ The Netatmo design is based on stations (usually the in-house module) and module Sensor design is not exactly the same for station and external modules and they are not addressed the same way wether in the station or an external module. This is a design issue of the API that restrict the ability to write generic code that could work for station sensor the same way than other modules sensors. The station role (the reporting device) and module role (getting environmental data) should not have been mixed. The fact that a sensor is physically built in the station should not interfere with this two distincts objects. -The consequence is that, for the API, we will use terms of station data (for the sensors inside the station) and module data (for external(s) module). Lookup methods like moduleByName look for external modules and **NOT station +The consequence is that, for the API, we will use terms of station data (for the sensors inside the station) and module data (for external(s) module). Lookup methods like module_by_name look for external modules and **NOT station modules**. Having two roles, the station has a 'station_name' property as well as a 'module_name' for its internal sensor. @@ -142,8 +142,8 @@ Constructor ```python authorization = pyatmo.ClientAuth( - clientId=CLIENT_ID, - clientSecret=CLIENT_SECRET, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, username=USERNAME, password=PASSWORD, scope="read_station", @@ -207,27 +207,27 @@ Properties, all properties are read-only unless specified: Methods : - * **stationByName** (station=None) : Find a station by it's station name + * **station_by_name** (station=None) : Find a station by it's station name * Input : Station name to lookup (str) * Output : station dictionary or None - * **stationById** (sid) : Find a station by it's Netatmo ID (mac address) + * **station_by_id** (sid) : Find a station by it's Netatmo ID (mac address) * Input : Station ID * Output : station dictionary or None - * **moduleByName** (module, station=None) : Find a module by it's module name + * **module_by_name** (module, station=None) : Find a module by it's module name * Input : module name and optional station name * Output : module dictionary or None The station name parameter, if provided, is used to check wether the module belongs to the appropriate station (in case multiple stations would have same module name). - * **moduleById** (mid, sid=None) : Find a module by it's ID and belonging station's ID + * **module_by_id** (mid, sid=None) : Find a module by it's ID and belonging station's ID * Input : module ID and optional Station ID * Output : module dictionary or None - * **modulesNamesList** (station=None) : Get the list of modules names, including the station module name. Each of them should have a corresponding entry in lastData. It is an equivalent (at lower cost) for lastData.keys() + * **modules_names_list** (station=None) : Get the list of modules names, including the station module name. Each of them should have a corresponding entry in last_data. It is an equivalent (at lower cost) for last_data.keys() - * **lastData** (station=None, exclude=0) : Get the last data uploaded by the station, exclude sensors with measurement older than given value (default return all) + * **last_data** (station=None, exclude=0) : Get the last data uploaded by the station, exclude sensors with measurement older than given value (default return all) * Input : station name OR id. If not provided default_station is used. Exclude is the delay in seconds from now to filter sensor readings. * Output : Sensors data dictionary (Key is sensor name) @@ -243,37 +243,37 @@ Methods : ```python # Last data access example -theData = weatherData.lastData() +theData = weatherData.last_data() print('Available modules : ', theData.keys()) print('In-house CO2 level : ', theData['indoor']['Co2']) print('Outside temperature : ', theData['outdoor']['Temperature']) print('External module battery : ', "OK" if int(theData['outdoor']['battery_vp']) > 5000 \ else "NEEDS TO BE REPLACED") ``` - * **checkNotUpdated** (station=None, delay=3600) : + * **check_not_updated** (station=None, delay=3600) : * Input : optional station name (else default_station is used) - * Output : list of modules name for which last data update is older than specified delay (default 1 hour). If the station itself is lost, the module_name of the station will be returned (the key item of lastData information). + * Output : list of modules name for which last data update is older than specified delay (default 1 hour). If the station itself is lost, the module_name of the station will be returned (the key item of last_data information). For example (following the previous one) ```python # Ensure data sanity -for m in weatherData.checkNotUpdated(""): +for m in weatherData.check_not_updated(""): print("Warning, sensor %s information is obsolete" % m) - if moduleByName(m) == None : # Sensor is not an external module + if module_by_name(m) == None : # Sensor is not an external module print("The station is lost") ``` - * **checkUpdated** (station=None, delay=3600) : + * **check_updated** (station=None, delay=3600) : * Input : optional station name (else default_station is used) * Output : list of modules name for which last data update is newer than specified delay (default 1 hour). Complement of the previous service - * **getMeasure** (device_id, scale, mtype, module_id=None, date_begin=None, date_end=None, limit=None, optimize=False) : + * **get_measure** (device_id, scale, mtype, module_id=None, date_begin=None, date_end=None, limit=None, optimize=False) : * Input : All parameters specified in the Netatmo API service GETMEASURE (type being a python reserved word as been replaced by mtype). * Output : A python dictionary reflecting the full service response. No transformation is applied. - * **MinMaxTH** (station=None, module=None, frame="last24") : Return min and max temperature and humidity for the given station/module in the given timeframe + * **min_max_th** (station=None, module=None, frame="last24") : Return min and max temperature and humidity for the given station/module in the given timeframe * Input : * An optional station Name or ID, default_station is used if not supplied, * An optional module name or ID, default : station sensor data is used @@ -320,54 +320,54 @@ Properties, all properties are read-only unless specified: Methods : - * **homeById** (hid) : Find a home by its Netatmo ID + * **home_by_id** (hid) : Find a home by its Netatmo ID * Input : Home ID * Output : home dictionary or None - * **homeByName** (home=None) : Find a home by it's home name + * **home_by_name** (home=None) : Find a home by it's home name * Input : home name to lookup (str) * Output : home dictionary or None - * **cameraById** (hid) : Find a camera by its Netatmo ID + * **camera_by_id** (hid) : Find a camera by its Netatmo ID * Input : camera ID * Output : camera dictionary or None - * **cameraByName** (camera=None, home=None) : Find a camera by it's camera name + * **camera_by_name** (camera=None, home=None) : Find a camera by it's camera name * Input : camera name and home name to lookup (str) * Output : camera dictionary or None - * **cameraType** (camera=None, home=None, cid=None) : Return the type of a given camera. + * **camera_type** (camera=None, home=None, cid=None) : Return the type of a given camera. * Input : camera name and home name or cameraID to lookup (str) * Output : Return the type of a given camera - * **cameraUrls** (camera=None, home=None, cid=None) : return Urls to access camera live feed + * **camera_urls_by_name** (camera=None, home=None, cid=None) : return Urls to access camera live feed * Input : camera name and home name or cameraID to lookup (str) * Output : tuple with the vpn_url (for remote access) and local url to access the camera live feed - * **personsAtHome** (home=None) : return the list of known persons who are at home + * **persons_at_home_by_name** (home=None) : return the list of known persons who are at home * Input : home name to lookup (str) * Output : list of persons seen - * **getCameraPicture** (image_id, key): Download a specific image (of an event or user face) from the camera + * **get_camera_picture** (image_id, key): Download a specific image (of an event or user face) from the camera * Input : image_id and key of an events or person face * Output: Tuple with image data (to be stored in a file) and image type (jpg, png...) - * **getProfileImage** (name) : Retrieve the face of a given person + * **get_profile_image** (name) : Retrieve the face of a given person * Input : person name (str) - * Output: **getCameraPicture** data + * Output: **get_camera_picture** data - * **updateEvent** (event=None, home=None, cameratype=None): Update the list of events + * **update_event** (event=None, home=None, cameratype=None): Update the list of events * Input: Id of the latest event, home name and cameratype to update event list - * **personSeenByCamera** (name, home=None, camera=None): Return true is a specific person has been seen by the camera in the last event + * **person_seen_by_camera** (name, home=None, camera=None): Return true is a specific person has been seen by the camera in the last event - * **someoneKnownSeen** (home=None, camera=None) : Return true is a known person has been in the last event + * **someone_known_seen** (home=None, camera=None) : Return true is a known person has been in the last event - * **someoneUnknownSeen** (home=None, camera=None) : Return true is an unknown person has been seen in the last event + * **someone_unknown_seen** (home=None, camera=None) : Return true is an unknown person has been seen in the last event - * **motionDetected** (home=None, camera=None) : Return true is a movement has been detected in the last event + * **motion_detected** (home=None, camera=None) : Return true is a movement has been detected in the last event - * **outdoormotionDetected** (home=None, camera=None) : Return true is a outdoor movement has been detected in the last event + * **outdoormotion_detected** (home=None, camera=None) : Return true is a outdoor movement has been detected in the last event * **humanDetected** (home=None, camera=None) : Return True if a human has been detected in the last outdoor events @@ -419,11 +419,11 @@ Methods : * Input : device name to lookup (str) * Output : device dictionary or None - * **moduleById** (hid) : Find a module by its Netatmo ID + * **module_by_id** (hid) : Find a module by its Netatmo ID * Input : module ID * Output : module dictionary or None - * **moduleByName** (module=None, device=None) : Find a module by it's module name + * **module_by_name** (module=None, device=None) : Find a module by it's module name * Input : module name and device name to lookup (str) * Output : module dictionary or None @@ -436,7 +436,7 @@ Methods : #### 4-6 Utilities functions #### - * **toTimeString** (timestamp) : Convert a Netatmo time stamp to a readable date/time format. - * **toEpoch**( dateString) : Convert a date string (form YYYY-MM-DD_HH:MM:SS) to timestamp - * **todayStamps**() : Return a couple of epoch time (start, end) for the current day + * **to_time_string** (timestamp) : Convert a Netatmo time stamp to a readable date/time format. + * **to_epoch**( dateString) : Convert a date string (form YYYY-MM-DD_HH:MM:SS) to timestamp + * **today_stamps**() : Return a couple of epoch time (start, end) for the current day From 6f1d14bd6a536e5cd071e56dc3390f9e31076b7f Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Wed, 19 Feb 2020 22:57:52 +0100 Subject: [PATCH 222/333] Set configurations for pylint and mypy Signed-off-by: Hugo D. (jabesq) --- .pylintrc | 19 +++---------------- setup.cfg | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.pylintrc b/.pylintrc index 434c83000..2ed283d65 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,32 +1,20 @@ [MESSAGES CONTROL] # Reasons disabled: -# locally-disabled - it spams too much # duplicate-code - unavoidable # cyclic-import - doesn't test if both import on load -# abstract-class-little-used - prevents from setting right foundation # unused-argument - generic callbacks and setup methods create a lot of warnings # global-statement - used for the on-demand requirement installation -# redefined-variable-type - this is Python, we're duck typing! # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing -# inconsistent-return-statements - doesn't handle raise # not-an-iterable - https://github.com/PyCQA/pylint/issues/2311 -# unnecessary-pass - readability for functions which only contain pass disable= - abstract-class-little-used, abstract-method, cyclic-import, duplicate-code, global-statement, inconsistent-return-statements, - invalid-name, - line-too-long, - locally-disabled, missing-docstring, - not-an-iterable, - not-context-manager, - redefined-variable-type, too-few-public-methods, too-many-arguments, too-many-branches, @@ -36,10 +24,9 @@ disable= too-many-public-methods, too-many-return-statements, too-many-statements, - unnecessary-pass, - unused-argument, - C0330, - W1203 + abstract-method, + not-an-iterable, + format, [REPORTS] reports=no diff --git a/setup.cfg b/setup.cfg index 8f76832a6..3006ec719 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,6 +11,19 @@ ignore = W503, E501 [mypy] ignore_errors = True +ignore_missing_imports = True + +[mypy-pyatmo.auth] +ignore_errors = False + +[mypy-pyatmo.camera] +ignore_errors = False + +[mypy-pyatmo.exceptions] +ignore_errors = False + +[mypy-pyatmo.helpers] +ignore_errors = False [isort] multi_line_output=3 @@ -23,3 +36,4 @@ combine_as_imports = true line_length=88 not_skip = __init__.py skip_glob = venv +known_first_party = pyatmo From 51f2e188bf42917b426efbb8cd192e7d030d361f Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Wed, 19 Feb 2020 23:02:35 +0100 Subject: [PATCH 223/333] Change fixId to fix_id Signed-off-by: Hugo D. (jabesq) --- src/pyatmo/auth.py | 8 +++++--- src/pyatmo/camera.py | 6 +++++- src/pyatmo/helpers.py | 8 ++++---- src/pyatmo/weather_station.py | 24 ++++++++++++------------ 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 8c52a71c3..0f8bebe16 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -104,7 +104,7 @@ def post_request( params = {} if "json" in params: - json_params = params.pop("json") + json_params: Optional[str] = params.pop("json") else: json_params = None @@ -147,7 +147,9 @@ def query(url, params, timeout, retries): if resp is None: LOG.debug("Resp is None - %s", resp) - elif not resp.ok: + return None + + if not resp.ok: LOG.debug("The Netatmo API returned %s", resp.status_code) LOG.debug("Netato API error: %s", resp.content) try: @@ -168,7 +170,7 @@ def query(url, params, timeout, retries): try: return ( resp.json() - if "application/json" in resp.headers.get("content-type") + if "application/json" in resp.headers.get("content-type", "") else resp.content ) except (TypeError, AttributeError): diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 1116a1341..7aca962f8 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -702,7 +702,11 @@ def set_state( Boolean -- Success of the request """ if home_id is None: - home_id = self.get_camera(camera_id)["home"] + _camera_data = self.get_camera(camera_id) + if _camera_data is None: + raise ValueError(f"Fails to retrieve camera data for id {camera_id}") + + home_id = _camera_data["home"] module = {"id": camera_id} diff --git a/src/pyatmo/helpers.py b/src/pyatmo/helpers.py index cf0de055e..ba02dd0d8 100644 --- a/src/pyatmo/helpers.py +++ b/src/pyatmo/helpers.py @@ -32,10 +32,10 @@ def today_stamps(): return today, today + 3600 * 24 -def fixId(rawData): - if rawData: - for station in rawData: +def fix_id(raw_data): + if raw_data: + for station in raw_data: station["_id"] = station["_id"].replace(" ", "") for module in station.get("modules", {}): module["_id"] = module["_id"].replace(" ", "") - return rawData + return raw_data diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 97b144ee5..b2c665f0a 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -2,7 +2,7 @@ import time from pyatmo.exceptions import NoDevice -from pyatmo.helpers import BASE_URL, fixId, today_stamps +from pyatmo.helpers import BASE_URL, fix_id, today_stamps LOG = logging.getLogger(__name__) @@ -26,7 +26,7 @@ def __init__(self, auth_data, url_req=None): if resp is None or "body" not in resp: raise NoDevice("No weather station data returned by Netatmo server") try: - self.raw_data = fixId(resp["body"].get("devices")) + self.raw_data = fix_id(resp["body"].get("devices")) except KeyError: LOG.debug("No in response %s", resp) raise NoDevice("No weather station data returned by Netatmo server") @@ -83,18 +83,18 @@ def get_modules(self, station_name=None, station_id=None): stations = [self.stations[station_data["_id"]]] else: stations = self.stations.values() - for s in stations: - res[s["_id"]] = { - "station_name": s["station_name"], - "module_name": s.get("module_name", s.get("type")), - "id": s["_id"], + for station in stations: + res[station["_id"]] = { + "station_name": station["station_name"], + "module_name": station.get("module_name", station.get("type")), + "id": station["_id"], } - for m in s["modules"]: - res[m["_id"]] = { - "station_name": m.get("station_name", s["station_name"]), - "module_name": m.get("module_name", m.get("type")), - "id": m["_id"], + for module in station.get("modules", {}): + res[module["_id"]] = { + "station_name": module.get("station_name", station["station_name"]), + "module_name": module.get("module_name", module.get("type")), + "id": module["_id"], } return res From 12bb6aea4b0830ea62c059fa34bcb2e792e01888 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sun, 10 May 2020 22:13:53 +0200 Subject: [PATCH 224/333] Fix issue with pylint and pytest Signed-off-by: Hugo D. (jabesq) --- src/pyatmo/auth.py | 2 +- src/pyatmo/thermostat.py | 8 ++++---- tests/test_pyatmo_camera.py | 4 ++-- tests/test_pyatmo_thermostat.py | 2 -- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 0f8bebe16..5b639183d 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -170,7 +170,7 @@ def query(url, params, timeout, retries): try: return ( resp.json() - if "application/json" in resp.headers.get("content-type", "") + if "application/json" in resp.headers.get("content-type", b"") else resp.content ) except (TypeError, AttributeError): diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index b23100480..ab8cb80a7 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -343,15 +343,15 @@ def thermostat_type(self, home, rid, home_id=None): return "NRV" def set_therm_mode(self, home_id, mode, end_time=None, schedule_id=None): - postParams = { + post_params = { "home_id": home_id, "mode": mode, } if end_time is not None and mode in ("hg", "away"): - postParams["endtime"] = end_time + post_params["endtime"] = end_time if schedule_id is not None and mode == "schedule": - postParams["schedule_id"] = schedule_id - return self.auth_data.post_request(url=_SETTHERMMODE_REQ, params=postParams) + post_params["schedule_id"] = schedule_id + return self.auth_data.post_request(url=_SETTHERMMODE_REQ, params=post_params) def set_room_therm_point(self, home_id, room_id, mode, temp=None, end_time=None): post_params = { diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 334bdd53e..50ddb7412 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -450,8 +450,8 @@ def test_camera_data_set_state( json_fixture, expected, ): - with open("fixtures/%s" % json_fixture) as f: - json_fixture = json.load(f) + with open("fixtures/%s" % json_fixture) as fixture_file: + json_fixture = json.load(fixture_file) requests_mock.post( pyatmo.camera._SETSTATE_REQ, json=json_fixture, diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index cacbfb10f..154fefd8f 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -398,7 +398,6 @@ def test_home_status_thermostat_type(home_status): def test_home_data_set_therm_mode( home_status, requests_mock, - caplog, home_id, mode, end_time, @@ -466,7 +465,6 @@ def test_home_data_set_therm_mode( def test_home_data_set_room_therm_point( home_status, requests_mock, - caplog, home_id, room_id, mode, From 03edb3e9fdfa0d3aeb69cdff8a92cea642e59445 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 14 May 2020 12:11:10 +0200 Subject: [PATCH 225/333] Fix JSON --- fixtures/home_data_simple.json | 1 - 1 file changed, 1 deletion(-) diff --git a/fixtures/home_data_simple.json b/fixtures/home_data_simple.json index ea6bda4f6..aecab9155 100644 --- a/fixtures/home_data_simple.json +++ b/fixtures/home_data_simple.json @@ -376,7 +376,6 @@ "hg_temp": 7, "away_temp": 14, "name": "Winter", - "selected": true, "id": "b1b54a2f45795764f59d50d8", "type": "therm" } From 5018f16aaab60535edcbf1edbef8e40798f38e63 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 14 May 2020 12:15:52 +0200 Subject: [PATCH 226/333] Section HomeData and HomeStatus --- src/pyatmo/thermostat.py | 142 +++++++++++++++-------------- tests/conftest.py | 11 +-- tests/test_pyatmo_thermostat.py | 154 ++++++++++++++++++++++++++------ 3 files changed, 199 insertions(+), 108 deletions(-) diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 52262e96b..4c6c350b2 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -113,18 +113,14 @@ def getSelectedschedule(self, home=None, home_id=None): if not home: home = self.default_home home_id = self.gethomeId(home=home) - return self.get_selected_schedule(home_id=home_id) + return self.get_selected_schedule(home_id) def get_selected_schedule(self, home_id: str): """Get the selected schedule for a given home ID.""" - try: - schedules = self.schedules[home_id] - except KeyError: - raise NoSchedule("No schedules available for %s" % home_id) - - for key in schedules.keys(): - if "selected" in schedules[key].keys(): - return schedules[key] + for key, value in self.schedules.get(home_id, {}).items(): + if "selected" in value.keys(): + return value + return {} def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): if home is None: @@ -147,7 +143,7 @@ def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): return self.switch_home_schedule(schedule_id=schedule_id, home_id=home_id) - def switch_home_schedule(self, schedule_id: str, home_id: str) -> bool: + def switch_home_schedule(self, schedule_id: str, home_id: str): """.""" try: schedules = self.schedules[home_id] @@ -170,21 +166,32 @@ def switch_home_schedule(self, schedule_id: str, home_id: str) -> bool: ) LOG.debug("Response: %s", resp) + def get_hg_temp(self, home_id: str) -> float: + """Return frost guard temperature value.""" + try: + data = self.get_selected_schedule(home_id) + except NoSchedule: + LOG.debug("No Schedule for Home ID %s", home_id) + return data.get("hg_temp") + + def get_away_temp(self, home_id: str) -> float: + try: + data = self.get_selected_schedule(home_id) + except NoSchedule: + LOG.debug("No Schedule for Home ID %s", home_id) + return data.get("away_temp") + + def get_thermostat_type(self, home_id: str, room_id: str): + for module in self.modules.get(home_id, {}).values(): + if module.get("room_id") == room_id: + return module.get("type") + class HomeStatus: - def __init__(self, authData, home_data=None, home_id=None, home=None): + def __init__(self, authData, home_id): self.authData = authData - if home_data is None: - self.home_data = HomeData(authData) - else: - self.home_data = home_data - if home_id is not None: - self.home_id = home_id - elif home is not None: - self.home_id = self.home_data.gethomeId(home=home) - else: - self.home_id = self.home_data.gethomeId(home=self.home_data.default_home) + self.home_id = home_id postParams = {"home_id": self.home_id} resp = self.authData.post_request(url=_GETHOMESTATUS_REQ, params=postParams) @@ -196,13 +203,16 @@ def __init__(self, authData, home_data=None, home_id=None, home=None): ): LOG.error("Errors in response: %s", resp) raise NoDevice("No device found, errors in response") + self.rawData = resp["body"]["home"] self.rooms = {} self.thermostats = {} self.valves = {} self.relays = {} + for r in self.rawData.get("rooms", []): self.rooms[r["id"]] = r + for module in self.rawData.get("modules", []): if module["type"] == "NATherm1": thermostatId = module["id"] @@ -219,56 +229,75 @@ def __init__(self, authData, home_data=None, home_id=None, home=None): if relayId not in self.relays: self.relays[relayId] = {} self.relays[relayId] = module + if self.rooms != {}: self.default_room = list(self.rooms.values())[0] + if self.relays != {}: self.default_relay = list(self.relays.values())[0] + if self.thermostats != {}: self.default_thermostat = list(self.thermostats.values())[0] + if self.valves != {}: self.default_valve = list(self.valves.values())[0] def roomById(self, rid): if not rid: return self.default_room + return self.get_room(rid) + + def get_room(self, room_id): for key, value in self.rooms.items(): - if value["id"] == rid: + if value["id"] == room_id: return self.rooms[key] - raise InvalidRoom("No room with ID %s" % rid) + raise InvalidRoom("No room with ID %s" % room_id) def thermostatById(self, rid): if not rid: return self.default_thermostat + return self.get_thermostat(rid) + + def get_thermostat(self, room_id: str): + """Return thermostat data for a given room id.""" for key, value in self.thermostats.items(): - if value["id"] == rid: + if value["id"] == room_id: return self.thermostats[key] - raise InvalidRoom("No room with ID %s" % rid) + raise InvalidRoom("No room with ID %s" % room_id) def relayById(self, rid): if not rid: return self.default_relay + return self.get_relay(rid) + + def get_relay(self, room_id: str): for key, value in self.relays.items(): - if value["id"] == rid: + if value["id"] == room_id: return self.relays[key] - raise InvalidRoom("No room with ID %s" % rid) + raise InvalidRoom("No room with ID %s" % room_id) def valveById(self, rid): if not rid: return self.default_valve + return self.get_valve(rid) + + def get_valve(self, room_id: str): for key, value in self.valves.items(): - if value["id"] == rid: + if value["id"] == room_id: return self.valves[key] - raise InvalidRoom("No room with ID %s" % rid) + raise InvalidRoom("No room with ID %s" % room_id) def setPoint(self, rid=None): """ Return the setpoint of a given room. """ - setpoint = None - room_data = self.roomById(rid=rid) - if room_data: - setpoint = room_data["therm_setpoint_temperature"] - return setpoint + if rid is None: + rid = self.default_room["id"] + return self.set_point(rid) + + def set_point(self, room_id: str): + """Return the setpoint of a given room.""" + return self.get_room(room_id).get("therm_setpoint_temperature") def setPointmode(self, rid=None): """ @@ -284,33 +313,11 @@ def setPointmode(self, rid=None): setpointmode = room_data["therm_setpoint_mode"] return setpointmode - def getAwaytemp(self, home=None, home_id=None): - if not home_id: - if not home: - home = self.home_data.default_home - try: - home_id = self.home_data.gethomeId(home) - except InvalidHome: - LOG.debug("No Schedule for Home ID %s", home_id) - return None - try: - data = self.home_data.getSelectedschedule(home_id=home_id) - except NoSchedule: - LOG.debug("No Schedule for Home ID %s", home_id) - return None - return data["away_temp"] - - def getHgtemp(self, home=None, home_id=None): - if not home_id: - if not home: - home = self.home_data.default_home - home_id = self.home_data.gethomeId(home) - try: - data = self.home_data.getSelectedschedule(home_id=home_id) - except NoSchedule: - LOG.debug("No Schedule for Home ID %s", home_id) - return None - return data["hg_temp"] + def set_point_mode(self, room_id): + """ + Return the setpointmode of a given room. + """ + return self.get_room(room_id).get("therm_setpoint_mode") def measuredTemperature(self, rid=None): """ @@ -332,17 +339,8 @@ def boilerStatus(self, rid=None): boiler_status = relay_status["boiler_status"] return boiler_status - def thermostatType(self, home, rid, home_id=None): - module_id = None - if home_id is None: - home_id = self.home_data.gethomeId(home=home) - for key in self.home_data.rooms[home_id]: - if key == rid: - for module_id in self.home_data.rooms[home_id][rid]["module_ids"]: - if module_id in self.thermostats: - return "NATherm1" - if module_id in self.valves: - return "NRV" + def boiler_status(self, room_id): + return self.thermostatById(room_id=room_id).get("boiler_status") def setThermmode(self, home_id, mode, end_time=None, schedule_id=None): postParams = { diff --git a/tests/conftest.py b/tests/conftest.py index 56abaa407..cf314455a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,7 +44,7 @@ def homeData(auth, requests_mock): @pytest.fixture(scope="function") -def homeStatus(auth, requests_mock): +def homeStatus(auth, home_id, requests_mock): with open("fixtures/home_status_simple.json") as f: json_fixture = json.load(f) requests_mock.post( @@ -52,14 +52,7 @@ def homeStatus(auth, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - with open("fixtures/home_data_simple.json") as f: - json_fixture = json.load(f) - requests_mock.post( - pyatmo.thermostat._GETHOMESDATA_REQ, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - return pyatmo.HomeStatus(auth) + return pyatmo.HomeStatus(auth, home_id) @pytest.fixture(scope="function") diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index 7c40a8afe..913794d83 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -127,6 +127,13 @@ def test_HomeData_getSelectedschedule(homeData): assert homeData.getSelectedschedule("Unknown") +def test_HomeData_get_selected_schedule(homeData): + assert ( + homeData.get_selected_schedule("91763b24c43d3e344f424e8b")["name"] == "Default" + ) + assert homeData.get_selected_schedule("Unknown") == {} + + @pytest.mark.parametrize( "t_home, t_sched_id, t_sched, expected", [ @@ -153,6 +160,33 @@ def test_HomeData_switchHomeSchedule( ) +def test_HomeData_get_away_temp(homeData): + assert homeData.get_away_temp("91763b24c43d3e344f424e8b") == 14 + assert homeData.get_away_temp("InvalidName") is None + assert homeData.get_away_temp("00000000000000000000000") is None + + +def test_HomeData_get_hg_temp(homeData): + assert homeData.get_hg_temp("91763b24c43d3e344f424e8b") == 7 + assert homeData.get_hg_temp("InvalidHome") is None + assert homeData.get_hg_temp("00000000000000000000000") is None + + +def test_HomeData_thermostat_type(homeData): + assert ( + homeData.get_thermostat_type("91763b24c43d3e344f424e8b", "2746182631") + == "NATherm1" + ) + assert ( + homeData.get_thermostat_type("91763b24c43d3e344f424e8b", "2833524037") == "NRV" + ) + assert homeData.get_thermostat_type("InvalidHome", "2833524037") is None + assert ( + homeData.get_thermostat_type("91763b24c43d3e344f424e8b", "0000000000") is None + ) + + +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus(homeStatus): assert len(homeStatus.rooms) == 3 assert homeStatus.default_room["id"] == "2746182631" @@ -184,7 +218,7 @@ def test_HomeStatus_error_and_data(auth, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - homeStatus = pyatmo.HomeStatus(auth) + homeStatus = pyatmo.HomeStatus(auth, home_id="91763b24c43d3e344f424e8b") assert len(homeStatus.rooms) == 3 assert homeStatus.default_room["id"] == "2746182631" @@ -216,9 +250,10 @@ def test_HomeStatus_error(auth, requests_mock): headers={"content-type": "application/json"}, ) with pytest.raises(pyatmo.NoDevice): - assert pyatmo.HomeStatus(auth) + assert pyatmo.HomeStatus(auth, home_id="91763b24c43d3e344f424e8b") +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_roomById(homeStatus): expexted = { "id": "2746182631", @@ -234,6 +269,23 @@ def test_HomeStatus_roomById(homeStatus): assert homeStatus.roomById("0000000000") +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) +def test_HomeStatus_get_room(homeStatus): + expexted = { + "id": "2746182631", + "reachable": True, + "therm_measured_temperature": 19.8, + "therm_setpoint_temperature": 12, + "therm_setpoint_mode": "away", + "therm_setpoint_start_time": 1559229567, + "therm_setpoint_end_time": 0, + } + assert homeStatus.get_room("2746182631") == expexted + with pytest.raises(pyatmo.InvalidRoom): + assert homeStatus.get_room("0000000000") + + +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_thermostatById(homeStatus): expexted = { "id": "12:34:56:00:01:ae", @@ -253,6 +305,27 @@ def test_HomeStatus_thermostatById(homeStatus): assert homeStatus.thermostatById("00:00:00:00:00:00") +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) +def test_HomeStatus_get_thermostat(homeStatus): + expexted = { + "id": "12:34:56:00:01:ae", + "reachable": True, + "type": "NATherm1", + "firmware_revision": 65, + "rf_strength": 58, + "battery_level": 3793, + "boiler_valve_comfort_boost": False, + "boiler_status": False, + "anticipating": False, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "high", + } + assert homeStatus.get_thermostat("12:34:56:00:01:ae") == expexted + with pytest.raises(pyatmo.InvalidRoom): + assert homeStatus.get_thermostat("00:00:00:00:00:00") + + +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_relayById(homeStatus): expexted = { "id": "12:34:56:00:fa:d0", @@ -266,6 +339,21 @@ def test_HomeStatus_relayById(homeStatus): assert homeStatus.relayById("00:00:00:00:00:00") +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) +def test_HomeStatus_get_relay(homeStatus): + expexted = { + "id": "12:34:56:00:fa:d0", + "type": "NAPlug", + "firmware_revision": 174, + "rf_strength": 107, + "wifi_strength": 42, + } + assert homeStatus.get_relay("12:34:56:00:fa:d0") == expexted + with pytest.raises(pyatmo.InvalidRoom): + assert homeStatus.get_relay("00:00:00:00:00:00") + + +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_valveById(homeStatus): expexted = { "id": "12:34:56:03:a5:54", @@ -282,6 +370,24 @@ def test_HomeStatus_valveById(homeStatus): assert homeStatus.valveById("00:00:00:00:00:00") +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) +def test_HomeStatus_get_valve(homeStatus): + expexted = { + "id": "12:34:56:03:a5:54", + "reachable": True, + "type": "NRV", + "firmware_revision": 79, + "rf_strength": 51, + "battery_level": 3025, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "full", + } + assert homeStatus.get_valve("12:34:56:03:a5:54") == expexted + with pytest.raises(pyatmo.InvalidRoom): + assert homeStatus.get_valve("00:00:00:00:00:00") + + +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_setPoint(homeStatus): assert homeStatus.setPoint() == 12 assert homeStatus.setPoint("2746182631") == 12 @@ -289,29 +395,28 @@ def test_HomeStatus_setPoint(homeStatus): assert homeStatus.setPoint("0000000000") +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) +def test_HomeStatus_set_point(homeStatus): + assert homeStatus.set_point("2746182631") == 12 + with pytest.raises(pyatmo.InvalidRoom): + assert homeStatus.set_point("0000000000") + + +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_setPointmode(homeStatus): assert homeStatus.setPointmode() == "away" assert homeStatus.setPointmode("2746182631") == "away" assert homeStatus.setPointmode("0000000000") is None -def test_HomeStatus_getAwaytemp(homeStatus): - assert homeStatus.getAwaytemp() == 14 - assert homeStatus.getAwaytemp("MYHOME") == 14 - assert homeStatus.getAwaytemp("InvalidName") is None - assert homeStatus.getAwaytemp(home_id="91763b24c43d3e344f424e8b") == 14 - assert homeStatus.getAwaytemp(home_id="00000000000000000000000") is None - - -def test_HomeStatus_getHgtemp(homeStatus): - assert homeStatus.getHgtemp() == 7 - assert homeStatus.getHgtemp("MYHOME") == 7 - with pytest.raises(pyatmo.InvalidHome): - assert homeStatus.getHgtemp("InvalidHome") - assert homeStatus.getHgtemp(home_id="91763b24c43d3e344f424e8b") == 7 - assert homeStatus.getHgtemp(home_id="00000000000000000000000") is None +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) +def test_HomeStatus_set_point_mode(homeStatus): + assert homeStatus.set_point_mode("2746182631") == "away" + with pytest.raises(pyatmo.InvalidRoom): + assert homeStatus.set_point_mode("0000000000") +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_measuredTemperature(homeStatus): assert homeStatus.measuredTemperature() == 19.8 assert homeStatus.measuredTemperature("2746182631") == 19.8 @@ -319,18 +424,11 @@ def test_HomeStatus_measuredTemperature(homeStatus): assert homeStatus.measuredTemperature("0000000000") +@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_boilerStatus(homeStatus): assert homeStatus.boilerStatus() is False -def test_HomeStatus_thermostatType(homeStatus): - assert homeStatus.thermostatType("MYHOME", "2746182631") == "NATherm1" - assert homeStatus.thermostatType("MYHOME", "2833524037") == "NRV" - with pytest.raises(pyatmo.InvalidHome): - assert homeStatus.thermostatType("InvalidHome", "2833524037") - assert homeStatus.thermostatType("MYHOME", "0000000000") is None - - @pytest.mark.parametrize( "home_id, mode, end_time, schedule_id, json_fixture, expected", [ @@ -552,7 +650,9 @@ def test_HomeData_setroomThermpoint_error( ) -def test_HomeStatus_error_disconnected(auth, requests_mock): +def test_HomeStatus_error_disconnected( + auth, requests_mock, home_id="91763b24c43d3e344f424e8b" +): with open("fixtures/home_status_error_disconnected.json") as f: json_fixture = json.load(f) requests_mock.post( @@ -568,4 +668,4 @@ def test_HomeStatus_error_disconnected(auth, requests_mock): headers={"content-type": "application/json"}, ) with pytest.raises(pyatmo.NoDevice): - pyatmo.HomeStatus(auth) + pyatmo.HomeStatus(auth, home_id) From f58bc5a04c2fd0e097a35ea592f1a69afa4abd05 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 14 May 2020 14:50:54 +0200 Subject: [PATCH 227/333] Remove old duplicate methods for cleaner interface --- src/pyatmo/thermostat.py | 122 +++++----------------------- tests/test_pyatmo_thermostat.py | 139 ++++++-------------------------- 2 files changed, 43 insertions(+), 218 deletions(-) diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 4c6c350b2..2807b0811 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -26,12 +26,15 @@ def __init__(self, authData): resp = self.authData.post_request(url=_GETHOMESDATA_REQ) if resp is None or "body" not in resp: raise NoDevice("No thermostat data returned by Netatmo server") + self.rawData = resp["body"].get("homes") if not self.rawData: raise NoDevice("No thermostat data available") + self.homes = {d["id"]: d for d in self.rawData} if not self.homes: raise NoDevice("No thermostat available") + self.modules = {} self.rooms = {} self.schedules = {} @@ -39,6 +42,7 @@ def __init__(self, authData): self.setpoint_duration = {} self.default_home = None self.default_home_id = None + for item in self.rawData: idHome = item.get("id") if not idHome: @@ -108,13 +112,6 @@ def getHomeName(self, home_id=None): return self.homes[key]["name"] raise InvalidHome("Invalid Home ID %s" % home_id) - def getSelectedschedule(self, home=None, home_id=None): - if not home_id: - if not home: - home = self.default_home - home_id = self.gethomeId(home=home) - return self.get_selected_schedule(home_id) - def get_selected_schedule(self, home_id: str): """Get the selected schedule for a given home ID.""" for key, value in self.schedules.get(home_id, {}).items(): @@ -122,28 +119,7 @@ def get_selected_schedule(self, home_id: str): return value return {} - def switchHomeSchedule(self, schedule_id=None, schedule=None, home=None): - if home is None: - home = self.default_home - home_id = self.gethomeId(home=home) - - schedules = { - self.schedules[home_id][s]["name"]: self.schedules[home_id][s]["id"] - for s in self.schedules[home_id] - } - if schedule is None and schedule_id is not None: - if schedule_id not in list(schedules.values()): - raise NoSchedule("%s is not a valid schedule id" % schedule_id) - elif schedule_id is None and schedule is not None: - if schedule not in list(schedules.keys()): - raise NoSchedule("%s is not a valid schedule" % schedule) - schedule_id = schedules[schedule] - else: - raise NoSchedule("No schedule specified") - - return self.switch_home_schedule(schedule_id=schedule_id, home_id=home_id) - - def switch_home_schedule(self, schedule_id: str, home_id: str): + def switch_home_schedule(self, home_id: str, schedule_id: str): """.""" try: schedules = self.schedules[home_id] @@ -242,109 +218,51 @@ def __init__(self, authData, home_id): if self.valves != {}: self.default_valve = list(self.valves.values())[0] - def roomById(self, rid): - if not rid: - return self.default_room - return self.get_room(rid) - def get_room(self, room_id): for key, value in self.rooms.items(): if value["id"] == room_id: return self.rooms[key] raise InvalidRoom("No room with ID %s" % room_id) - def thermostatById(self, rid): - if not rid: - return self.default_thermostat - return self.get_thermostat(rid) - def get_thermostat(self, room_id: str): """Return thermostat data for a given room id.""" + print(self.rawData) for key, value in self.thermostats.items(): + print(key, value) if value["id"] == room_id: return self.thermostats[key] raise InvalidRoom("No room with ID %s" % room_id) - def relayById(self, rid): - if not rid: - return self.default_relay - return self.get_relay(rid) - def get_relay(self, room_id: str): for key, value in self.relays.items(): if value["id"] == room_id: return self.relays[key] raise InvalidRoom("No room with ID %s" % room_id) - def valveById(self, rid): - if not rid: - return self.default_valve - return self.get_valve(rid) - def get_valve(self, room_id: str): for key, value in self.valves.items(): if value["id"] == room_id: return self.valves[key] raise InvalidRoom("No room with ID %s" % room_id) - def setPoint(self, rid=None): - """ - Return the setpoint of a given room. - """ - if rid is None: - rid = self.default_room["id"] - return self.set_point(rid) - def set_point(self, room_id: str): """Return the setpoint of a given room.""" return self.get_room(room_id).get("therm_setpoint_temperature") - def setPointmode(self, rid=None): - """ - Return the setpointmode of a given room. - """ - setpointmode = None - try: - room_data = self.roomById(rid=rid) - except InvalidRoom: - LOG.debug("Invalid room %s", rid) - room_data = None - if room_data: - setpointmode = room_data["therm_setpoint_mode"] - return setpointmode - - def set_point_mode(self, room_id): - """ - Return the setpointmode of a given room. - """ + def set_point_mode(self, room_id: str): + """Return the setpointmode of a given room.""" return self.get_room(room_id).get("therm_setpoint_mode") - def measuredTemperature(self, rid=None): - """ - Return the measured temperature of a given room. - """ - temperature = None - room_data = self.roomById(rid=rid) - if room_data: - temperature = room_data.get("therm_measured_temperature") - return temperature - - def boilerStatus(self, rid=None): - boiler_status = None - if rid: - relay_status = self.thermostatById(rid=rid) - else: - relay_status = self.thermostatById(rid=None) - if relay_status: - boiler_status = relay_status["boiler_status"] - return boiler_status - - def boiler_status(self, room_id): - return self.thermostatById(room_id=room_id).get("boiler_status") - - def setThermmode(self, home_id, mode, end_time=None, schedule_id=None): + def measured_temperature(self, room_id: str): + """Return the measured temperature of a given room.""" + return self.get_room(room_id).get("therm_measured_temperature") + + def boiler_status(self, module_id: str): + return self.get_thermostat(module_id).get("boiler_status") + + def setThermmode(self, mode, end_time=None, schedule_id=None): postParams = { - "home_id": home_id, + "home_id": self.home_id, "mode": mode, } if end_time is not None and mode in ("hg", "away"): @@ -353,9 +271,9 @@ def setThermmode(self, home_id, mode, end_time=None, schedule_id=None): postParams["schedule_id"] = schedule_id return self.authData.post_request(url=_SETTHERMMODE_REQ, params=postParams) - def setroomThermpoint(self, home_id, room_id, mode, temp=None, end_time=None): + def setroomThermpoint(self, room_id: str, mode: str, temp=None, end_time=None): postParams = { - "home_id": home_id, + "home_id": self.home_id, "room_id": room_id, "mode": mode, } diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index 913794d83..81fbcc706 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -120,13 +120,6 @@ def test_HomeData_getHomeName(homeData): assert homeData.getHomeName(home_id) == "Unknown" -def test_HomeData_getSelectedschedule(homeData): - assert homeData.getSelectedschedule()["name"] == "Default" - assert homeData.getSelectedschedule("MYHOME")["name"] == "Default" - with pytest.raises(pyatmo.InvalidHome): - assert homeData.getSelectedschedule("Unknown") - - def test_HomeData_get_selected_schedule(homeData): assert ( homeData.get_selected_schedule("91763b24c43d3e344f424e8b")["name"] == "Default" @@ -135,17 +128,18 @@ def test_HomeData_get_selected_schedule(homeData): @pytest.mark.parametrize( - "t_home, t_sched_id, t_sched, expected", + "t_home_id, t_sched_id, expected", [ - (None, None, None, pytest.raises(pyatmo.NoSchedule)), - (None, None, "Default", does_not_raise()), - (None, "591b54a2764ff4d50d8b5795", None, does_not_raise()), - (None, None, "Summer", pytest.raises(pyatmo.NoSchedule)), - (None, "123456789abcdefg12345678", None, pytest.raises(pyatmo.NoSchedule)), + ("91763b24c43d3e344f424e8b", "591b54a2764ff4d50d8b5795", does_not_raise()), + ( + "91763b24c43d3e344f424e8b", + "123456789abcdefg12345678", + pytest.raises(pyatmo.NoSchedule), + ), ], ) -def test_HomeData_switchHomeSchedule( - homeData, requests_mock, t_home, t_sched_id, t_sched, expected +def test_HomeData_switch_home_schedule( + homeData, requests_mock, t_home_id, t_sched_id, expected ): with open("fixtures/status_ok.json") as f: json_fixture = json.load(f) @@ -155,9 +149,7 @@ def test_HomeData_switchHomeSchedule( headers={"content-type": "application/json"}, ) with expected: - homeData.switchHomeSchedule( - schedule_id=t_sched_id, schedule=t_sched, home=t_home - ) + homeData.switch_home_schedule(home_id=t_home_id, schedule_id=t_sched_id) def test_HomeData_get_away_temp(homeData): @@ -253,22 +245,6 @@ def test_HomeStatus_error(auth, requests_mock): assert pyatmo.HomeStatus(auth, home_id="91763b24c43d3e344f424e8b") -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_roomById(homeStatus): - expexted = { - "id": "2746182631", - "reachable": True, - "therm_measured_temperature": 19.8, - "therm_setpoint_temperature": 12, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 1559229567, - "therm_setpoint_end_time": 0, - } - assert homeStatus.roomById("2746182631") == expexted - with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.roomById("0000000000") - - @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_get_room(homeStatus): expexted = { @@ -285,26 +261,6 @@ def test_HomeStatus_get_room(homeStatus): assert homeStatus.get_room("0000000000") -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_thermostatById(homeStatus): - expexted = { - "id": "12:34:56:00:01:ae", - "reachable": True, - "type": "NATherm1", - "firmware_revision": 65, - "rf_strength": 58, - "battery_level": 3793, - "boiler_valve_comfort_boost": False, - "boiler_status": False, - "anticipating": False, - "bridge": "12:34:56:00:fa:d0", - "battery_state": "high", - } - assert homeStatus.thermostatById("12:34:56:00:01:ae") == expexted - with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.thermostatById("00:00:00:00:00:00") - - @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_get_thermostat(homeStatus): expexted = { @@ -325,20 +281,6 @@ def test_HomeStatus_get_thermostat(homeStatus): assert homeStatus.get_thermostat("00:00:00:00:00:00") -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_relayById(homeStatus): - expexted = { - "id": "12:34:56:00:fa:d0", - "type": "NAPlug", - "firmware_revision": 174, - "rf_strength": 107, - "wifi_strength": 42, - } - assert homeStatus.relayById("12:34:56:00:fa:d0") == expexted - with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.relayById("00:00:00:00:00:00") - - @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_get_relay(homeStatus): expexted = { @@ -353,23 +295,6 @@ def test_HomeStatus_get_relay(homeStatus): assert homeStatus.get_relay("00:00:00:00:00:00") -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_valveById(homeStatus): - expexted = { - "id": "12:34:56:03:a5:54", - "reachable": True, - "type": "NRV", - "firmware_revision": 79, - "rf_strength": 51, - "battery_level": 3025, - "bridge": "12:34:56:00:fa:d0", - "battery_state": "full", - } - assert homeStatus.valveById("12:34:56:03:a5:54") == expexted - with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.valveById("00:00:00:00:00:00") - - @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_get_valve(homeStatus): expexted = { @@ -387,14 +312,6 @@ def test_HomeStatus_get_valve(homeStatus): assert homeStatus.get_valve("00:00:00:00:00:00") -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_setPoint(homeStatus): - assert homeStatus.setPoint() == 12 - assert homeStatus.setPoint("2746182631") == 12 - with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.setPoint("0000000000") - - @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_set_point(homeStatus): assert homeStatus.set_point("2746182631") == 12 @@ -402,13 +319,6 @@ def test_HomeStatus_set_point(homeStatus): assert homeStatus.set_point("0000000000") -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_setPointmode(homeStatus): - assert homeStatus.setPointmode() == "away" - assert homeStatus.setPointmode("2746182631") == "away" - assert homeStatus.setPointmode("0000000000") is None - - @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) def test_HomeStatus_set_point_mode(homeStatus): assert homeStatus.set_point_mode("2746182631") == "away" @@ -417,16 +327,15 @@ def test_HomeStatus_set_point_mode(homeStatus): @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_measuredTemperature(homeStatus): - assert homeStatus.measuredTemperature() == 19.8 - assert homeStatus.measuredTemperature("2746182631") == 19.8 +def test_HomeStatus_measured_temperature(homeStatus): + assert homeStatus.measured_temperature("2746182631") == 19.8 with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.measuredTemperature("0000000000") + assert homeStatus.measured_temperature("0000000000") @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_boilerStatus(homeStatus): - assert homeStatus.boilerStatus() is False +def test_HomeStatus_boiler_status(homeStatus): + assert homeStatus.boiler_status("12:34:56:00:01:ae") is False @pytest.mark.parametrize( @@ -492,7 +401,7 @@ def test_HomeStatus_boilerStatus(homeStatus): ), ], ) -def test_HomeData_setThermmode( +def test_HomeStatus_setThermmode( homeStatus, requests_mock, caplog, @@ -510,9 +419,7 @@ def test_HomeData_setThermmode( json=json_fixture, headers={"content-type": "application/json"}, ) - res = homeStatus.setThermmode( - home_id=home_id, mode=mode, end_time=end_time, schedule_id=schedule_id - ) + res = homeStatus.setThermmode(mode=mode, end_time=end_time, schedule_id=schedule_id) if "error" in res: assert expected in res["error"]["message"] else: @@ -560,7 +467,7 @@ def test_HomeData_setThermmode( ), ], ) -def test_HomeData_setroomThermpoint( +def test_HomeStatus_setroomThermpoint( homeStatus, requests_mock, caplog, @@ -581,7 +488,7 @@ def test_HomeData_setroomThermpoint( ) assert ( homeStatus.setroomThermpoint( - home_id=home_id, room_id=room_id, mode=mode, temp=temp, end_time=end_time + room_id=room_id, mode=mode, temp=temp, end_time=end_time )["status"] == expected ) @@ -624,7 +531,7 @@ def test_HomeData_setroomThermpoint( ), ], ) -def test_HomeData_setroomThermpoint_error( +def test_HomeStatus_setroomThermpoint_error( homeStatus, requests_mock, caplog, @@ -643,9 +550,9 @@ def test_HomeData_setroomThermpoint_error( headers={"content-type": "application/json"}, ) assert ( - homeStatus.setroomThermpoint( - home_id=home_id, room_id=room_id, mode=mode, temp=temp - )["error"]["message"] + homeStatus.setroomThermpoint(room_id=room_id, mode=mode, temp=temp)["error"][ + "message" + ] == expected ) From 616b5fb71bd3bec751a6c79f381d42e5e79f313a Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 14 May 2020 15:21:10 +0200 Subject: [PATCH 228/333] Clean up --- src/pyatmo/thermostat.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 2807b0811..ccd3733a5 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -226,9 +226,7 @@ def get_room(self, room_id): def get_thermostat(self, room_id: str): """Return thermostat data for a given room id.""" - print(self.rawData) for key, value in self.thermostats.items(): - print(key, value) if value["id"] == room_id: return self.thermostats[key] raise InvalidRoom("No room with ID %s" % room_id) From b2d7e070c4717c4bc8daac8847a9a611906ee963 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 27 May 2020 00:47:40 +0200 Subject: [PATCH 229/333] Remove default values as they are unreliable --- src/pyatmo/thermostat.py | 50 +++------------------------------------- 1 file changed, 3 insertions(+), 47 deletions(-) diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index ccd3733a5..66c2058e5 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -1,6 +1,6 @@ import logging -from .exceptions import InvalidHome, InvalidRoom, NoDevice, NoSchedule +from .exceptions import InvalidRoom, NoDevice, NoSchedule from .helpers import _BASE_URL LOG = logging.getLogger(__name__) @@ -15,10 +15,10 @@ class HomeData: """ - List the Energy devices (relays, thermostat modules and valves) + List of energy devices (relays, thermostat modules and valves) Args: - authData (ClientAuth): Authentication information with a working access Token + authData (ClientAuth): Authentication information with a valid access token """ def __init__(self, authData): @@ -40,8 +40,6 @@ def __init__(self, authData): self.schedules = {} self.zones = {} self.setpoint_duration = {} - self.default_home = None - self.default_home_id = None for item in self.rawData: idHome = item.get("id") @@ -73,8 +71,6 @@ def __init__(self, authData): for room in item["rooms"]: self.rooms[idHome][room["id"]] = room if "therm_schedules" in item: - self.default_home = nameHome - self.default_home_id = item["id"] for schedule in item["therm_schedules"]: self.schedules[idHome][schedule["id"]] = schedule for schedule in item["therm_schedules"]: @@ -84,34 +80,6 @@ def __init__(self, authData): for zone in schedule["zones"]: self.zones[idHome][scheduleId][zone["id"]] = zone - def homeById(self, hid): - return None if hid not in self.homes else self.homes[hid] - - def homeByName(self, home=None): - if not home: - home = self.default_home - for key, value in self.homes.items(): - if value["name"] == home: - return self.homes[key] - raise InvalidHome("Invalid Home %s" % home) - - def gethomeId(self, home=None): - if not home: - home = self.default_home - for key, value in self.homes.items(): - if value["name"] == home: - if "therm_schedules" in self.homes[key]: - return self.homes[key]["id"] - raise InvalidHome("Invalid Home %s" % home) - - def getHomeName(self, home_id=None): - if home_id is None: - home_id = self.default_home_id - for key, value in self.homes.items(): - if value["id"] == home_id: - return self.homes[key]["name"] - raise InvalidHome("Invalid Home ID %s" % home_id) - def get_selected_schedule(self, home_id: str): """Get the selected schedule for a given home ID.""" for key, value in self.schedules.get(home_id, {}).items(): @@ -206,18 +174,6 @@ def __init__(self, authData, home_id): self.relays[relayId] = {} self.relays[relayId] = module - if self.rooms != {}: - self.default_room = list(self.rooms.values())[0] - - if self.relays != {}: - self.default_relay = list(self.relays.values())[0] - - if self.thermostats != {}: - self.default_thermostat = list(self.thermostats.values())[0] - - if self.valves != {}: - self.default_valve = list(self.valves.values())[0] - def get_room(self, room_id): for key, value in self.rooms.items(): if value["id"] == room_id: From d38ed6ae19df11fb96a59231cec0505e6c645d1a Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 27 May 2020 10:46:09 +0200 Subject: [PATCH 230/333] Remove unused tests --- tests/test_pyatmo_thermostat.py | 91 ++++++++++++++------------------- 1 file changed, 39 insertions(+), 52 deletions(-) diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index 81fbcc706..ea0dc7710 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -9,12 +9,6 @@ def test_HomeData(homeData): - assert homeData.default_home == "MYHOME" - assert homeData.default_home_id == "91763b24c43d3e344f424e8b" - assert len(homeData.rooms[homeData.default_home_id]) == 4 - - assert len(homeData.modules[homeData.default_home_id]) == 5 - expected = { "12:34:56:00:fa:d0": { "id": "12:34:56:00:fa:d0", @@ -59,7 +53,7 @@ def test_HomeData(homeData): "room_id": "3688132631", }, } - assert homeData.modules[homeData.default_home_id] == expected + assert homeData.modules["91763b24c43d3e344f424e8b"] == expected def test_HomeData_no_data(auth, requests_mock): @@ -90,34 +84,22 @@ def test_HomeData_no_home_name(auth, requests_mock): ) homeData = pyatmo.HomeData(auth) home_id = "91763b24c43d3e344f424e8b" - assert homeData.homeById(home_id)["name"] == "Unknown" + assert homeData.homes.get(home_id)["name"] == "Unknown" -def test_HomeData_homeById(homeData): +def test_HomeData_homes_by_id(homeData): home_id = "91763b24c43d3e344f424e8b" - assert homeData.homeById(home_id)["name"] == "MYHOME" + assert homeData.homes.get(home_id)["name"] == "MYHOME" home_id = "91763b24c43d3e344f424e8c" - assert homeData.homeById(home_id)["name"] == "Unknown" - - -def test_HomeData_homeByName(homeData): - assert homeData.homeByName()["name"] == "MYHOME" - assert homeData.homeByName()["id"] == "91763b24c43d3e344f424e8b" - + assert homeData.homes.get(home_id)["name"] == "Unknown" -def test_HomeData_gethomeId(homeData): - assert homeData.gethomeId() == "91763b24c43d3e344f424e8b" - assert homeData.gethomeId("MYHOME") == "91763b24c43d3e344f424e8b" - with pytest.raises(pyatmo.InvalidHome): - assert homeData.gethomeId("InvalidName") - -def test_HomeData_getHomeName(homeData): - assert homeData.getHomeName() == "MYHOME" - home_id = "91763b24c43d3e344f424e8b" - assert homeData.getHomeName(home_id) == "MYHOME" - home_id = "91763b24c43d3e344f424e8c" - assert homeData.getHomeName(home_id) == "Unknown" +@pytest.mark.parametrize( + "home_id, expected", + [("91763b24c43d3e344f424e8b", "MYHOME"), ("91763b24c43d3e344f424e8c", "Unknown")], +) +def test_HomeData_get_home_name(homeData, home_id, expected): + assert homeData.homes[home_id]["name"] == expected def test_HomeData_get_selected_schedule(homeData): @@ -178,21 +160,27 @@ def test_HomeData_thermostat_type(homeData): ) -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus(homeStatus): +@pytest.mark.parametrize( + "home_id, room_id, expected", + [ + ( + "91763b24c43d3e344f424e8b", + "2746182631", + { + "id": "2746182631", + "reachable": True, + "therm_measured_temperature": 19.8, + "therm_setpoint_temperature": 12, + "therm_setpoint_mode": "away", + "therm_setpoint_start_time": 1559229567, + "therm_setpoint_end_time": 0, + }, + ), + ], +) +def test_HomeStatus(homeStatus, room_id, expected): assert len(homeStatus.rooms) == 3 - assert homeStatus.default_room["id"] == "2746182631" - - expexted = { - "id": "2746182631", - "reachable": True, - "therm_measured_temperature": 19.8, - "therm_setpoint_temperature": 12, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 1559229567, - "therm_setpoint_end_time": 0, - } - assert homeStatus.default_room == expexted + assert homeStatus.rooms[room_id] == expected def test_HomeStatus_error_and_data(auth, requests_mock): @@ -203,16 +191,15 @@ def test_HomeStatus_error_and_data(auth, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - with open("fixtures/home_data_simple.json") as f: - json_fixture = json.load(f) - requests_mock.post( - pyatmo.thermostat._GETHOMESDATA_REQ, - json=json_fixture, - headers={"content-type": "application/json"}, - ) + # with open("fixtures/home_data_simple.json") as f: + # json_fixture = json.load(f) + # requests_mock.post( + # pyatmo.thermostat._GETHOMESDATA_REQ, + # json=json_fixture, + # headers={"content-type": "application/json"}, + # ) homeStatus = pyatmo.HomeStatus(auth, home_id="91763b24c43d3e344f424e8b") assert len(homeStatus.rooms) == 3 - assert homeStatus.default_room["id"] == "2746182631" expexted = { "id": "2746182631", @@ -223,7 +210,7 @@ def test_HomeStatus_error_and_data(auth, requests_mock): "therm_setpoint_start_time": 1559229567, "therm_setpoint_end_time": 0, } - assert homeStatus.default_room == expexted + assert homeStatus.rooms["2746182631"] == expexted def test_HomeStatus_error(auth, requests_mock): From ae5437c0c435903de512dc824c6b872fc49ed84a Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 27 May 2020 10:48:32 +0200 Subject: [PATCH 231/333] Remove old duplicate methods for cleaner interface --- src/pyatmo/camera.py | 288 +++++------------------------------- tests/test_pyatmo_camera.py | 282 +++++++++++------------------------ 2 files changed, 117 insertions(+), 453 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 247cb0d50..4f46a19ce 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -17,11 +17,11 @@ class CameraData: """ - List the Netatmo camera informations + List of Netatmo camera informations (Homes, cameras, smoke detectors, modules, events, persons) Args: authData (ClientAuth): - Authentication information with a working access Token + Authentication information with a valid access token """ def __init__(self, authData, size=15): @@ -45,10 +45,6 @@ def __init__(self, authData, size=15): self.lastEvent = {} self.outdoor_lastEvent = {} self.types = {} - self.default_home = None - self.default_home_id = None - self.default_camera = None - self.default_smokedetector = None for item in self.rawData: homeId = item.get("id") nameHome = item.get("name") @@ -67,9 +63,6 @@ def __init__(self, authData, size=15): for p in item["persons"]: self.persons[p["id"]] = p if "events" in item: - if not self.default_home and not self.default_home_id: - self.default_home = item["name"] - self.default_home_id = item["id"] for e in item["events"]: if e["type"] == "outdoor": if e["camera_id"] not in self.outdoor_events: @@ -81,7 +74,7 @@ def __init__(self, authData, size=15): self.events[e["camera_id"]][e["time"]] = e for c in item["cameras"]: self.cameras[homeId][c["id"]] = c - self.cameras[homeId][c["id"]]["home"] = homeId + self.cameras[homeId][c["id"]]["home_id"] = homeId if c["type"] == "NACamera" and "modules" in c: for m in c["modules"]: self.modules[m["id"]] = m @@ -100,43 +93,10 @@ def __init__(self, authData, size=15): self.outdoor_lastEvent[camera] = self.outdoor_events[camera][ sorted(self.outdoor_events[camera])[-1] ] - if self.modules != {}: - self.default_module = list(self.modules.values())[0]["name"] - else: - self.default_module = None - if self.default_home is not None and self.cameras[self.default_home_id]: - self.default_camera = list(self.cameras[self.default_home_id].values())[0] - - def homeById(self, hid): - return None if hid not in self.homes else self.homes[hid] - - def homeByName(self, home=None): - if not home: - return self.homeByName(self.default_home) - for key, value in self.homes.items(): - if value["name"] == home: - return self.homes[key] - raise InvalidHome() - - def getHomeName(self, home_id=None): - if home_id is None: - home_id = self.default_home_id - for key, value in self.homes.items(): - if value["id"] == home_id: - return self.homes[key]["name"] - raise InvalidHome("Invalid Home ID %s" % home_id) - - def gethomeId(self, home=None): - if not home: - home = self.default_home - for key, value in self.homes.items(): - if value["name"] == home: - return self.homes[key]["id"] - raise InvalidHome("Invalid Home %s" % home) - - def cameraById(self, cid): - """Get camera data by ID.""" - return self.get_camera(cid) + + for home_id in self.homes: + for camera_id in self.cameras[home_id]: + self.update_camera_urls(camera_id) def get_camera(self, cid: str) -> Dict[str, str]: """Get camera data.""" @@ -145,126 +105,29 @@ def get_camera(self, cid: str) -> Dict[str, str]: return self.cameras[home_id][cid] return None - def cameraByName(self, camera=None, home=None, home_id=None): - """Get camera data by name.""" - if home_id is None: - if home is None: - hid = self.default_home_id - else: - try: - hid = self.homeByName(home)["id"] - except InvalidHome: - LOG.debug("Invalid home %s", home) - return None - else: - hid = home_id - if camera is None and home is None and home_id is None: - return self.default_camera - if not (home_id or home) and camera: - for h_id, cam_ids in self.cameras.items(): - for cam_id in cam_ids: - if self.cameras[h_id][cam_id]["name"] == camera: - return self.cameras[h_id][cam_id] - elif hid and camera: - if hid not in self.cameras: - return None - for cam_id in self.cameras[hid]: - if self.cameras[hid][cam_id]["name"] == camera: - return self.cameras[hid][cam_id] - else: - return list(self.cameras[hid].values())[0] - - def moduleById(self, mid): + def get_module(self, mid: str): + """Get module data.""" return None if mid not in self.modules else self.modules[mid] - def moduleByName(self, module=None, camera=None, home=None): - if not module: - if self.default_module: - return self.moduleByName(self.default_module) - return None - cam = None - if camera or home: - cam = self.cameraByName(camera, home) - if not cam: - return None - for key, value in self.modules.items(): - if value["name"] == module: - if cam and value["cam_id"] != cam["id"]: - return None - return self.modules[key] - return None - - def smokedetectorById(self, sid): + def get_smokedetector(self, sid: str): + """Get smoke detector.""" for home, sd in self.smokedetectors.items(): if sid in self.smokedetectors[home]: return self.smokedetectors[home][sid] return None - def smokedetectorByName(self, smokedetector=None, home=None, home_id=None): - if home_id is None: - if home is None: - hid = self.default_home_id - else: - try: - hid = self.homeByName(home)["id"] - except InvalidHome: - LOG.debug("Invalid home %s", home) - return None - else: - hid = home_id - if smokedetector is None and home is None and home_id is None: - return self.default_smokedetector - elif not (home_id or home) and smokedetector: - for h_id, cam_ids in self.smokedetectors.items(): - for cam_id in cam_ids: - if self.smokedetectors[h_id][cam_id]["name"] == smokedetector: - return self.smokedetectors[h_id][cam_id] - elif hid and smokedetector: - hid = self.homeByName(home)["id"] - if hid not in self.smokedetectors: - return None - for cam_id in self.smokedetectors[hid]: - if self.smokedetectors[hid][cam_id]["name"] == smokedetector: - return self.smokedetectors[hid][cam_id] - else: - return list(self.smokedetectors[hid].values())[0] - - def cameraType(self, camera=None, home=None, cid=None, home_id=None): - """ - Return the type of a given camera. - """ - cameratype = None - if cid: - camera_data = self.get_camera(cid) - else: - camera_data = self.cameraByName(camera=camera, home=home, home_id=home_id) - if camera_data: - cameratype = camera_data["type"] - return cameratype - - def cameraUrls( - self, camera: str = None, home: str = None, cid: str = None, home_id: str = None - ) -> Tuple[str, str]: - """ - Return the vpn_url and the local_url (if available) of a given camera - in order to access its live feed - (old interface) - """ - if home_id: - cid = self.cameraByName(camera=camera, home_id=home_id)["id"] - else: - cid = self.cameraByName(camera=camera, home=home)["id"] - return self.camera_urls(cid=cid) - def camera_urls(self, cid: str) -> Tuple[str, str]: """ Return the vpn_url and the local_url (if available) of a given camera in order to access its live feed """ - local_url = None - vpn_url = None + camera_data = self.get_camera(cid) + return camera_data.get("vpn_url", None), camera_data.get("local_url", None) + def update_camera_urls(self, cid: str) -> None: + """Update and validate the camera urls.""" camera_data = self.get_camera(cid) + home_id = camera_data["home_id"] if camera_data: vpn_url = camera_data.get("vpn_url") @@ -282,43 +145,22 @@ def check_url(url): return resp.get("local_url") temp_local_url = check_url(vpn_url) - local_url = check_url(temp_local_url) - return vpn_url, local_url + self.cameras[home_id][cid]["local_url"] = check_url(temp_local_url) def get_light_state(self, cid: str) -> str: """Return the current mode of the floodlight of a presence camera.""" return self.get_camera(cid).get("light_mode_status") - def personsAtHome(self, home=None, home_id=None): - """ - Return the list of known persons who are currently at home - (old interface) - """ - if not home: - home_id = self.default_home_id - else: - home_id = self.homeByName(home)["id"] - return self.persons_at_home(home_id) - def persons_at_home(self, home_id=None): - """ - Return the list of known persons who are currently at home - """ - home_data = self.homeById(home_id) - atHome = [] + """Return a list of known persons who are currently at home.""" + home_data = self.homes.get(home_id) + at_home = [] for p in home_data["persons"]: # Only check known personshome if "pseudo" in p: if not p["out_of_sight"]: - atHome.append(p["pseudo"]) - return atHome - - def setPersonsHome(self, person_ids, home_id): - """ - Mark persons as home. - (old interface) - """ - return self.set_persons_home(person_ids, home_id) + at_home.append(p["pseudo"]) + return at_home def set_persons_home(self, person_ids, home_id): """ @@ -331,13 +173,6 @@ def set_persons_home(self, person_ids, home_id): resp = self.authData.post_request(url=_SETPERSONSHOME_REQ, params=postParams) return resp - def setPersonsAway(self, person_id, home_id): - """ - Mark a person as away or set the whole home to being empty. - (old interface) - """ - return self.set_persons_away(person_id, home_id) - def set_persons_away(self, person_id, home_id): """Mark a person as away or set the whole home to being empty. @@ -355,13 +190,6 @@ def set_persons_away(self, person_id, home_id): resp = self.authData.post_request(url=_SETPERSONSAWAY_REQ, params=postParams) return resp - def getPersonId(self, name): - """ - Retrieve a persons id - (old interface) - """ - return self.get_person_id(name) - def get_person_id(self, name): """Retrieve the ID of a person @@ -482,18 +310,6 @@ def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): sorted(self.outdoor_events[camera])[-1] ] - def personSeenByCamera(self, name, home=None, camera=None, exclude=0): - """ - Evaluate if a specific person has been seen - (old interface) - """ - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("personSeenByCamera: Camera name or home is unknown") - return False - return self.person_seen_by_camera(name=name, cid=cid, exclude=exclude) - def person_seen_by_camera(self, name, cid, exclude=0): """ Evaluate if a specific person has been seen @@ -533,23 +349,10 @@ def knownPersonsNames(self): names.append(p["pseudo"]) return names - def someoneKnownSeen(self, home=None, camera=None, exclude=0, cid=None): - """ - Evaluate if someone known has been seen - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("someoneKnownSeen: Camera name or home is unknown") - return False - return self.someone_known_seen(cid=cid, exclude=exclude) - - def someone_known_seen(self, cid, exclude=0): - """ - Evaluate if someone known has been seen - """ + def someone_known_seen(self, cid: str, exclude: int = 0) -> bool: + """Evaluate if someone known has been seen.""" + if cid not in self.events: + raise NoDevice if exclude: limit = time.time() - exclude array_time_event = sorted(self.events[cid], reverse=True) @@ -565,20 +368,10 @@ def someone_known_seen(self, cid, exclude=0): return True return False - def someoneUnknownSeen(self, home=None, camera=None, exclude=0, cid=None): - """ - Evaluate if someone unknown has been seen - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("someoneUnknownSeen: Camera name or home is unknown") - return False - return self.someone_unknown_seen(cid=cid, exclude=exclude) - - def someone_unknown_seen(self, cid, exclude=0): + def someone_unknown_seen(self, cid: str, exclude: int = 0) -> bool: + """Evaluate if someone known has been seen.""" + if cid not in self.events: + raise NoDevice if exclude: limit = time.time() - exclude array_time_event = sorted(self.events[cid], reverse=True) @@ -597,23 +390,10 @@ def someone_unknown_seen(self, cid, exclude=0): return True return False - def motionDetected(self, home=None, camera=None, exclude=0, cid=None): - """ - Evaluate if movement has been detected - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("motionDetected: Camera name or home is unknown") - return False - return self.motion_detected(cid=cid, exclude=exclude) - def motion_detected(self, cid, exclude=0): - """ - Evaluate if movement has been detected - """ + """Evaluate if movement has been detected.""" + if cid not in self.events: + raise NoDevice if exclude: limit = time.time() - exclude array_time_event = sorted(self.events[cid], reverse=True) @@ -811,7 +591,7 @@ def set_state( Boolean -- Success of the request """ if home_id is None: - home_id = self.get_camera(camera_id)["home"] + home_id = self.get_camera(camera_id)["home_id"] module = {"id": camera_id} diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 6e0dbf9f8..4c3df7946 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -1,4 +1,5 @@ """Define tests for Camera module.""" +from contextlib import nullcontext as does_not_raise import json import pytest @@ -10,41 +11,7 @@ def test_CameraData(cameraHomeData): - assert cameraHomeData.default_home == "MYHOME" - assert cameraHomeData.default_camera["id"] == "12:34:56:00:f1:62" - assert cameraHomeData.default_camera["name"] == "Hall" - - -@pytest.mark.parametrize( - "hid, expected", - [ - ("91763b24c43d3e344f424e8b", "MYHOME"), - (INVALID_NAME, "MYHOME"), - pytest.param(None, None), - ], -) -def test_CameraData_homeById(cameraHomeData, hid, expected): - if hid is None or hid == INVALID_NAME: - assert cameraHomeData.homeById(hid) is None - else: - assert cameraHomeData.homeById(hid)["name"] == expected - - -@pytest.mark.parametrize( - "name, expected", - [ - ("MYHOME", "91763b24c43d3e344f424e8b"), - (None, "91763b24c43d3e344f424e8b"), - ("", "91763b24c43d3e344f424e8b"), - pytest.param(INVALID_NAME, None), - ], -) -def test_CameraData_homeByName(cameraHomeData, name, expected): - if name == INVALID_NAME: - with pytest.raises(pyatmo.exceptions.InvalidHome): - assert cameraHomeData.homeByName(name) - else: - assert cameraHomeData.homeByName(name)["id"] == expected + assert cameraHomeData.homes is not None @pytest.mark.parametrize( @@ -56,69 +23,20 @@ def test_CameraData_homeByName(cameraHomeData, name, expected): (None, None), ], ) -def test_CameraData_cameraById(cameraHomeData, cid, expected): - camera = cameraHomeData.cameraById(cid) +def test_CameraData_get_camera(cameraHomeData, cid, expected): + camera = cameraHomeData.get_camera(cid) if camera: assert camera["name"] == expected else: assert camera is expected -@pytest.mark.parametrize( - "name, home, home_id, expected", - [ - ("Hall", None, None, "12:34:56:00:f1:62"), - (None, None, None, "12:34:56:00:f1:62"), - ("", None, None, "12:34:56:00:f1:62"), - ("Hall", "MYHOME", None, "12:34:56:00:f1:62"), - ("Hall", None, "91763b24c43d3e344f424e8b", "12:34:56:00:f1:62"), - (None, None, "91763b24c43d3e344f424e8b", "12:34:56:00:f1:62"), - (None, "MYHOME", None, "12:34:56:00:f1:62"), - ("", "MYHOME", None, "12:34:56:00:f1:62"), - ("Garden", "MYHOME", None, "12:34:56:00:a5:a4"), - ("Garden", None, "InvalidHomeID", "12:34:56:00:a5:a4"), - (INVALID_NAME, None, None, None), - (None, INVALID_NAME, None, None), - ], -) -def test_CameraData_cameraByName(cameraHomeData, name, home, home_id, expected): - if home == INVALID_NAME or name == INVALID_NAME or home_id == "InvalidHomeID": - assert cameraHomeData.cameraByName(name, home, home_id) is None - elif home_id is None: - assert cameraHomeData.cameraByName(name, home)["id"] == expected - elif home is None: - assert cameraHomeData.cameraByName(name, home_id=home_id)["id"] == expected - else: - assert cameraHomeData.cameraByName(name, home, home_id)["id"] == expected - - -def test_CameraData_moduleById(cameraHomeData): - assert cameraHomeData.moduleById("00:00:00:00:00:00") is None - - -def test_CameraData_moduleByName(cameraHomeData): - assert cameraHomeData.moduleByName() is None - - -@pytest.mark.parametrize( - "camera, home, cid, expected", - [ - (None, None, None, "NACamera"), - ("Hall", None, None, "NACamera"), - ("Hall", "MYHOME", None, "NACamera"), - (None, "MYHOME", None, "NACamera"), - (None, "MYHOME", "12:34:56:00:f1:62", "NACamera"), - (None, None, "12:34:56:00:f1:62", "NACamera"), - ("Garden", None, None, "NOC"), - (INVALID_NAME, None, None, None), - pytest.param(None, INVALID_NAME, None, None), - ], -) -def test_CameraData_cameraType(cameraHomeData, camera, home, cid, expected): - assert cameraHomeData.cameraType(camera, home, cid) == expected +def test_CameraData_get_module(cameraHomeData): + assert cameraHomeData.get_module("00:00:00:00:00:00") is None -def test_CameraData_cameraUrls(cameraHomeData, requests_mock): +def test_CameraData_camera_urls(cameraHomeData, requests_mock): + cid = "12:34:56:00:f1:62" vpn_url = ( "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/" "6d278460699e56180d47ab47169efb31/" @@ -139,10 +57,11 @@ def test_CameraData_cameraUrls(cameraHomeData, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - assert cameraHomeData.cameraUrls() == (vpn_url, local_url) + cameraHomeData.update_camera_urls(cid) + assert cameraHomeData.camera_urls(cid) == (vpn_url, local_url) -def test_CameraData_cameraUrls_disconnected(auth, requests_mock): +def test_CameraData_camera_urls_disconnected(auth, requests_mock): with open("fixtures/camera_home_data_disconnected.json") as f: json_fixture = json.load(f) requests_mock.post( @@ -151,43 +70,53 @@ def test_CameraData_cameraUrls_disconnected(auth, requests_mock): headers={"content-type": "application/json"}, ) camera_data = pyatmo.CameraData(auth) - assert camera_data.cameraUrls() == (None, None) + + cid = "12:34:56:00:f1:62" + vpn_url = ( + "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/" + "6d278460699e56180d47ab47169efb31/" + "MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTg,," + ) + local_url = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" + with open("fixtures/camera_ping.json") as f: + json_fixture = json.load(f) + requests_mock.post( + vpn_url + "/command/ping", + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with open("fixtures/camera_ping.json") as f: + json_fixture = json.load(f) + requests_mock.post( + local_url + "/command/ping", + json=json_fixture, + headers={"content-type": "application/json"}, + ) + camera_data.update_camera_urls(cid) + assert camera_data.camera_urls(cid) == (None, None) @pytest.mark.parametrize( - "home, expected", - [ - (None, ["Richard Doe"]), - ("MYHOME", ["Richard Doe"]), - pytest.param( - INVALID_NAME, - None, - # marks=pytest.mark.xfail(reason="Invalid home name not handled yet"), - ), - ], + "home_id, expected", [("91763b24c43d3e344f424e8b", ["Richard Doe"])], ) -def test_CameraData_personsAtHome(cameraHomeData, home, expected): - if home == INVALID_NAME: - with pytest.raises(pyatmo.exceptions.InvalidHome): - assert cameraHomeData.personsAtHome(home) - else: - assert cameraHomeData.personsAtHome(home) == expected +def test_CameraData_persons_at_home(cameraHomeData, home_id, expected): + assert cameraHomeData.persons_at_home(home_id) == expected @freeze_time("2019-06-16") @pytest.mark.parametrize( - "name, exclude, expected", + "name, cid, exclude, expected", [ - ("John Doe", None, True), - ("Richard Doe", None, False), - ("Unknown", None, False), - ("John Doe", 1, False), - ("John Doe", 50000, True), - ("Jack Doe", None, False), + ("John Doe", "12:34:56:00:f1:62", None, True), + ("Richard Doe", "12:34:56:00:f1:62", None, False), + ("Unknown", "12:34:56:00:f1:62", None, False), + ("John Doe", "12:34:56:00:f1:62", 1, False), + ("John Doe", "12:34:56:00:f1:62", 50000, True), + ("Jack Doe", "12:34:56:00:f1:62", None, False), ], ) -def test_CameraData_personSeenByCamera(cameraHomeData, name, exclude, expected): - assert cameraHomeData.personSeenByCamera(name, exclude=exclude) is expected +def test_CameraData_person_seen_by_camera(cameraHomeData, name, cid, exclude, expected): + assert cameraHomeData.person_seen_by_camera(name, cid, exclude=exclude) is expected def test_CameraData__knownPersons(cameraHomeData): @@ -212,8 +141,8 @@ def test_CameraData_knownPersonsNames(cameraHomeData): ("Richard Doe", "91827376-7e04-5298-83af-a0cb8372dff3"), ], ) -def test_CameraData_getPersonId(cameraHomeData, name, expected): - assert cameraHomeData.getPersonId(name) == expected +def test_CameraData_get_person_id(cameraHomeData, name, expected): + assert cameraHomeData.get_person_id(name) == expected @pytest.mark.parametrize( @@ -233,7 +162,7 @@ def test_CameraData_getPersonId(cameraHomeData, name, expected): ), ], ) -def test_CameraData_setPersonsAway( +def test_CameraData_set_persons_away( cameraHomeData, requests_mock, hid, pid, json_fixture, expected ): with open("fixtures/%s" % json_fixture) as f: @@ -243,7 +172,7 @@ def test_CameraData_setPersonsAway( json=json_fixture, headers={"content-type": "application/json"}, ) - assert cameraHomeData.setPersonsAway(pid, hid)["status"] == expected + assert cameraHomeData.set_persons_away(pid, hid)["status"] == expected @pytest.mark.parametrize( @@ -266,7 +195,7 @@ def test_CameraData_setPersonsAway( ), ], ) -def test_CameraData_setPersonsHome( +def test_CameraData_set_persons_home( cameraHomeData, requests_mock, hid, pids, json_fixture, expected ): with open("fixtures/%s" % json_fixture) as f: @@ -276,68 +205,56 @@ def test_CameraData_setPersonsHome( json=json_fixture, headers={"content-type": "application/json"}, ) - assert cameraHomeData.setPersonsHome(pids, hid)["status"] == expected + assert cameraHomeData.set_persons_home(pids, hid)["status"] == expected @freeze_time("2019-06-16") @pytest.mark.parametrize( - "home, camera, exclude, expected", + "camera_id, exclude, expected,expectation", [ - (None, None, None, True), - (None, None, 5, False), - (None, "InvalidCamera", None, False), - ("InvalidHome", None, None, False), + ("12:34:56:00:f1:62", None, True, does_not_raise()), + ("12:34:56:00:f1:62", 5, False, does_not_raise()), + (None, None, None, pytest.raises(pyatmo.NoDevice)), ], ) -def test_CameraData_someoneKnownSeen(cameraHomeData, home, camera, exclude, expected): - assert cameraHomeData.someoneKnownSeen(home, camera, exclude) == expected +def test_CameraData_someone_known_seen( + cameraHomeData, camera_id, exclude, expected, expectation +): + with expectation: + assert cameraHomeData.someone_known_seen(camera_id, exclude) == expected @freeze_time("2019-06-16") @pytest.mark.parametrize( - "home, camera, exclude, expected", + "camera_id, exclude, expected, expectation", [ - (None, None, None, False), - (None, None, 100, False), - (None, INVALID_NAME, None, False), - (INVALID_NAME, None, None, False), + ("12:34:56:00:f1:62", None, False, does_not_raise()), + ("12:34:56:00:f1:62", 100, False, does_not_raise()), + (None, None, None, pytest.raises(pyatmo.NoDevice)), ], ) -def test_CameraData_someoneUnknownSeen(cameraHomeData, home, camera, exclude, expected): - assert cameraHomeData.someoneUnknownSeen(home, camera, exclude) == expected +def test_CameraData_someone_unknown_seen( + cameraHomeData, camera_id, exclude, expected, expectation +): + with expectation: + assert cameraHomeData.someone_unknown_seen(camera_id, exclude) == expected @freeze_time("2019-06-16") @pytest.mark.parametrize( - "home, camera, exclude, expected", + "camera_id, exclude, expected, expectation", [ - (None, None, None, False), - (None, None, 140000, True), - (None, None, 130000, False), - (None, INVALID_NAME, None, False), - (INVALID_NAME, None, None, False), + ("12:34:56:00:f1:62", None, False, does_not_raise()), + ("12:34:56:00:f1:62", 140000, True, does_not_raise()), + ("12:34:56:00:f1:62", 130000, False, does_not_raise()), + (None, None, False, pytest.raises(pyatmo.NoDevice)), ], ) -def test_CameraData_motionDetected(cameraHomeData, home, camera, exclude, expected): - assert cameraHomeData.motionDetected(home, camera, exclude) == expected - - -def test_CameraData_getHomeName(cameraHomeData): - assert cameraHomeData.getHomeName() == "MYHOME" - home_id = "91763b24c43d3e344f424e8b" - assert cameraHomeData.getHomeName(home_id) == "MYHOME" - home_id = "91763b24c43d3e344f424e8c" - assert cameraHomeData.getHomeName(home_id) == "Unknown" - home_id = "InvalidHomeID" - with pytest.raises(pyatmo.InvalidHome): - assert cameraHomeData.getHomeName(home_id) == "Unknown" - - -def test_CameraData_gethomeId(cameraHomeData): - assert cameraHomeData.gethomeId() == "91763b24c43d3e344f424e8b" - assert cameraHomeData.gethomeId("MYHOME") == "91763b24c43d3e344f424e8b" - with pytest.raises(pyatmo.InvalidHome): - assert cameraHomeData.gethomeId("InvalidName") +def test_CameraData_motion_detected( + cameraHomeData, camera_id, exclude, expected, expectation +): + with expectation: + assert cameraHomeData.motion_detected(camera_id, exclude) == expected @pytest.mark.parametrize( @@ -349,47 +266,14 @@ def test_CameraData_gethomeId(cameraHomeData): (None, None), ], ) -def test_CameraData_smokedetectorById(cameraHomeData, sid, expected): - smokedetector = cameraHomeData.smokedetectorById(sid) +def test_CameraData_get_smokedetector(cameraHomeData, sid, expected): + smokedetector = cameraHomeData.get_smokedetector(sid) if smokedetector: assert smokedetector["name"] == expected else: assert smokedetector is expected -@pytest.mark.parametrize( - "name, home, home_id, expected", - [ - ("Hall", None, None, "12:34:56:00:8b:a2"), - (None, None, None, None), - ("", None, None, "12:34:56:00:8b:a2"), - ("Hall", "MYHOME", None, "12:34:56:00:8b:a2"), - ("Hall", None, "91763b24c43d3e344f424e8b", "12:34:56:00:8b:a2"), - (None, None, "91763b24c43d3e344f424e8b", "12:34:56:00:8b:a2"), - (None, "MYHOME", None, "12:34:56:00:8b:a2"), - ("", "MYHOME", None, "12:34:56:00:8b:a2"), - ("Kitchen", "MYHOME", None, "12:34:56:00:8b:ac"), - (INVALID_NAME, None, None, None), - (None, INVALID_NAME, None, None), - ], -) -def test_CameraData_smokedetectorByName(cameraHomeData, name, home, home_id, expected): - if ( - home == INVALID_NAME - or name == INVALID_NAME - or (name is None and home is None and home_id is None) - ): - assert cameraHomeData.smokedetectorByName(name, home, home_id) is None - elif home_id is None: - assert cameraHomeData.smokedetectorByName(name, home)["id"] == expected - elif home is None: - assert ( - cameraHomeData.smokedetectorByName(name, home_id=home_id)["id"] == expected - ) - else: - assert cameraHomeData.smokedetectorByName(name, home, home_id)["id"] == expected - - @pytest.mark.parametrize( "home_id, camera_id, floodlight, monitoring, json_fixture, expected", [ From fbb7b615489b427c9afb10b8b3e1cf68f0c1144d Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 27 May 2020 11:49:31 +0200 Subject: [PATCH 232/333] Clean up thermostat code and tests --- fixtures/home_data_no_devices.json | 31 ++++++++++++++ fixtures/home_data_no_homes.json | 18 ++++++++ src/pyatmo/thermostat.py | 68 ++++++++++++------------------ tests/test_pyatmo_thermostat.py | 26 +++++++++--- 4 files changed, 95 insertions(+), 48 deletions(-) create mode 100644 fixtures/home_data_no_devices.json create mode 100644 fixtures/home_data_no_homes.json diff --git a/fixtures/home_data_no_devices.json b/fixtures/home_data_no_devices.json new file mode 100644 index 000000000..583644367 --- /dev/null +++ b/fixtures/home_data_no_devices.json @@ -0,0 +1,31 @@ +{ + "body": { + "homes": [ + { + "id": "91763b24c43d3e344f424e8c", + "altitude": 112, + "coordinates": [ + 52.516263, + 13.377726 + ], + "country": "DE", + "timezone": "Europe/Berlin", + "therm_setpoint_default_duration": 180, + "therm_mode": "schedule" + } + ], + "user": { + "email": "john@doe.com", + "language": "de-DE", + "locale": "de-DE", + "feel_like_algorithm": 0, + "unit_pressure": 0, + "unit_system": 0, + "unit_wind": 0, + "id": "91763b24c43d3e344f424e8b" + } + }, + "status": "ok", + "time_exec": 0.056135892868042, + "time_server": 1559171003 +} \ No newline at end of file diff --git a/fixtures/home_data_no_homes.json b/fixtures/home_data_no_homes.json new file mode 100644 index 000000000..486695ae2 --- /dev/null +++ b/fixtures/home_data_no_homes.json @@ -0,0 +1,18 @@ +{ + "body": { + "homes": [], + "user": { + "email": "john@doe.com", + "language": "de-DE", + "locale": "de-DE", + "feel_like_algorithm": 0, + "unit_pressure": 0, + "unit_system": 0, + "unit_wind": 0, + "id": "91763b24c43d3e344f424e8b" + } + }, + "status": "ok", + "time_exec": 0.056135892868042, + "time_server": 1559171003 +} \ No newline at end of file diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 66c2058e5..dd6ec382a 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -32,8 +32,6 @@ def __init__(self, authData): raise NoDevice("No thermostat data available") self.homes = {d["id"]: d for d in self.rawData} - if not self.homes: - raise NoDevice("No thermostat available") self.modules = {} self.rooms = {} @@ -42,43 +40,40 @@ def __init__(self, authData): self.setpoint_duration = {} for item in self.rawData: - idHome = item.get("id") - if not idHome: - LOG.error('No key ["id"] in %s', item.keys()) - continue + home_id = item.get("id") nameHome = item.get("name") if not nameHome: nameHome = "Unknown" - self.homes[idHome]["name"] = nameHome + self.homes[home_id]["name"] = nameHome if "modules" in item: - if idHome not in self.modules: - self.modules[idHome] = {} + if home_id not in self.modules: + self.modules[home_id] = {} for m in item["modules"]: - self.modules[idHome][m["id"]] = m - if idHome not in self.rooms: - self.rooms[idHome] = {} - if idHome not in self.schedules: - self.schedules[idHome] = {} - if idHome not in self.zones: - self.zones[idHome] = {} - if idHome not in self.setpoint_duration: - self.setpoint_duration[idHome] = {} + self.modules[home_id][m["id"]] = m + if home_id not in self.rooms: + self.rooms[home_id] = {} + if home_id not in self.schedules: + self.schedules[home_id] = {} + if home_id not in self.zones: + self.zones[home_id] = {} + if home_id not in self.setpoint_duration: + self.setpoint_duration[home_id] = {} if "therm_setpoint_default_duration" in item: - self.setpoint_duration[idHome] = item[ + self.setpoint_duration[home_id] = item[ "therm_setpoint_default_duration" ] if "rooms" in item: for room in item["rooms"]: - self.rooms[idHome][room["id"]] = room + self.rooms[home_id][room["id"]] = room if "therm_schedules" in item: for schedule in item["therm_schedules"]: - self.schedules[idHome][schedule["id"]] = schedule + self.schedules[home_id][schedule["id"]] = schedule for schedule in item["therm_schedules"]: scheduleId = schedule["id"] - if scheduleId not in self.zones[idHome]: - self.zones[idHome][scheduleId] = {} + if scheduleId not in self.zones[home_id]: + self.zones[home_id][scheduleId] = {} for zone in schedule["zones"]: - self.zones[idHome][scheduleId][zone["id"]] = zone + self.zones[home_id][scheduleId][zone["id"]] = zone def get_selected_schedule(self, home_id: str): """Get the selected schedule for a given home ID.""" @@ -89,14 +84,9 @@ def get_selected_schedule(self, home_id: str): def switch_home_schedule(self, home_id: str, schedule_id: str): """.""" - try: - schedules = self.schedules[home_id] - except KeyError: - raise NoSchedule("No schedules available for %s" % home_id) - schedules = { self.schedules[home_id][s]["name"]: self.schedules[home_id][s]["id"] - for s in self.schedules[home_id] + for s in self.schedules.get(home_id) } if schedule_id not in list(schedules.values()): raise NoSchedule("%s is not a valid schedule id" % schedule_id) @@ -112,20 +102,14 @@ def switch_home_schedule(self, home_id: str, schedule_id: str): def get_hg_temp(self, home_id: str) -> float: """Return frost guard temperature value.""" - try: - data = self.get_selected_schedule(home_id) - except NoSchedule: - LOG.debug("No Schedule for Home ID %s", home_id) - return data.get("hg_temp") + return self.get_selected_schedule(home_id).get("hg_temp") def get_away_temp(self, home_id: str) -> float: - try: - data = self.get_selected_schedule(home_id) - except NoSchedule: - LOG.debug("No Schedule for Home ID %s", home_id) - return data.get("away_temp") + """Return the configured away temperature value.""" + return self.get_selected_schedule(home_id).get("away_temp") def get_thermostat_type(self, home_id: str, room_id: str): + """Return the thermostat type of the room.""" for module in self.modules.get(home_id, {}).values(): if module.get("room_id") == room_id: return module.get("type") @@ -214,7 +198,7 @@ def measured_temperature(self, room_id: str): def boiler_status(self, module_id: str): return self.get_thermostat(module_id).get("boiler_status") - def setThermmode(self, mode, end_time=None, schedule_id=None): + def set_thermmode(self, mode, end_time=None, schedule_id=None): postParams = { "home_id": self.home_id, "mode": mode, @@ -225,7 +209,7 @@ def setThermmode(self, mode, end_time=None, schedule_id=None): postParams["schedule_id"] = schedule_id return self.authData.post_request(url=_SETTHERMMODE_REQ, params=postParams) - def setroomThermpoint(self, room_id: str, mode: str, temp=None, end_time=None): + def set_room_thermpoint(self, room_id: str, mode: str, temp=None, end_time=None): postParams = { "home_id": self.home_id, "room_id": room_id, diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index ea0dc7710..22a5a1eb4 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -74,6 +74,18 @@ def test_HomeData_no_body(auth, requests_mock): assert pyatmo.HomeData(auth) +def test_HomeData_no_homes(auth, requests_mock): + with open("fixtures/home_data_no_homes.json") as f: + json_fixture = json.load(f) + requests_mock.post( + pyatmo.thermostat._GETHOMESDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with pytest.raises(pyatmo.NoDevice): + assert pyatmo.HomeData(auth) + + def test_HomeData_no_home_name(auth, requests_mock): with open("fixtures/home_data_nohomename.json") as f: json_fixture = json.load(f) @@ -388,7 +400,7 @@ def test_HomeStatus_boiler_status(homeStatus): ), ], ) -def test_HomeStatus_setThermmode( +def test_HomeStatus_set_thermmode( homeStatus, requests_mock, caplog, @@ -406,7 +418,9 @@ def test_HomeStatus_setThermmode( json=json_fixture, headers={"content-type": "application/json"}, ) - res = homeStatus.setThermmode(mode=mode, end_time=end_time, schedule_id=schedule_id) + res = homeStatus.set_thermmode( + mode=mode, end_time=end_time, schedule_id=schedule_id + ) if "error" in res: assert expected in res["error"]["message"] else: @@ -454,7 +468,7 @@ def test_HomeStatus_setThermmode( ), ], ) -def test_HomeStatus_setroomThermpoint( +def test_HomeStatus_set_room_thermpoint( homeStatus, requests_mock, caplog, @@ -474,7 +488,7 @@ def test_HomeStatus_setroomThermpoint( headers={"content-type": "application/json"}, ) assert ( - homeStatus.setroomThermpoint( + homeStatus.set_room_thermpoint( room_id=room_id, mode=mode, temp=temp, end_time=end_time )["status"] == expected @@ -518,7 +532,7 @@ def test_HomeStatus_setroomThermpoint( ), ], ) -def test_HomeStatus_setroomThermpoint_error( +def test_HomeStatus_set_room_thermpoint_error( homeStatus, requests_mock, caplog, @@ -537,7 +551,7 @@ def test_HomeStatus_setroomThermpoint_error( headers={"content-type": "application/json"}, ) assert ( - homeStatus.setroomThermpoint(room_id=room_id, mode=mode, temp=temp)["error"][ + homeStatus.set_room_thermpoint(room_id=room_id, mode=mode, temp=temp)["error"][ "message" ] == expected From 25e36c51c76d38bd4147186ddb0bf78ed96aca7f Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 27 May 2020 12:28:57 +0200 Subject: [PATCH 233/333] Clean up thermostat tests --- tests/test_pyatmo_thermostat.py | 58 +++++++++++++-------------------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index 22a5a1eb4..df3778b39 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -99,18 +99,11 @@ def test_HomeData_no_home_name(auth, requests_mock): assert homeData.homes.get(home_id)["name"] == "Unknown" -def test_HomeData_homes_by_id(homeData): - home_id = "91763b24c43d3e344f424e8b" - assert homeData.homes.get(home_id)["name"] == "MYHOME" - home_id = "91763b24c43d3e344f424e8c" - assert homeData.homes.get(home_id)["name"] == "Unknown" - - @pytest.mark.parametrize( "home_id, expected", [("91763b24c43d3e344f424e8b", "MYHOME"), ("91763b24c43d3e344f424e8c", "Unknown")], ) -def test_HomeData_get_home_name(homeData, home_id, expected): +def test_HomeData_homes_by_id(homeData, home_id, expected): assert homeData.homes[home_id]["name"] == expected @@ -146,30 +139,32 @@ def test_HomeData_switch_home_schedule( homeData.switch_home_schedule(home_id=t_home_id, schedule_id=t_sched_id) -def test_HomeData_get_away_temp(homeData): - assert homeData.get_away_temp("91763b24c43d3e344f424e8b") == 14 - assert homeData.get_away_temp("InvalidName") is None - assert homeData.get_away_temp("00000000000000000000000") is None +@pytest.mark.parametrize( + "home_id, expected", + [("91763b24c43d3e344f424e8b", 14), ("00000000000000000000000", None)], +) +def test_HomeData_get_away_temp(homeData, home_id, expected): + assert homeData.get_away_temp(home_id) == expected -def test_HomeData_get_hg_temp(homeData): - assert homeData.get_hg_temp("91763b24c43d3e344f424e8b") == 7 - assert homeData.get_hg_temp("InvalidHome") is None - assert homeData.get_hg_temp("00000000000000000000000") is None +@pytest.mark.parametrize( + "home_id, expected", + [("91763b24c43d3e344f424e8b", 7), ("00000000000000000000000", None)], +) +def test_HomeData_get_hg_temp(homeData, home_id, expected): + assert homeData.get_hg_temp(home_id) == expected -def test_HomeData_thermostat_type(homeData): - assert ( - homeData.get_thermostat_type("91763b24c43d3e344f424e8b", "2746182631") - == "NATherm1" - ) - assert ( - homeData.get_thermostat_type("91763b24c43d3e344f424e8b", "2833524037") == "NRV" - ) - assert homeData.get_thermostat_type("InvalidHome", "2833524037") is None - assert ( - homeData.get_thermostat_type("91763b24c43d3e344f424e8b", "0000000000") is None - ) +@pytest.mark.parametrize( + "home_id, module_id, expected", + [ + ("91763b24c43d3e344f424e8b", "2746182631", "NATherm1"), + ("91763b24c43d3e344f424e8b", "2833524037", "NRV"), + ("91763b24c43d3e344f424e8b", "0000000000", None), + ], +) +def test_HomeData_thermostat_type(homeData, home_id, module_id, expected): + assert homeData.get_thermostat_type(home_id, module_id) == expected @pytest.mark.parametrize( @@ -203,13 +198,6 @@ def test_HomeStatus_error_and_data(auth, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - # with open("fixtures/home_data_simple.json") as f: - # json_fixture = json.load(f) - # requests_mock.post( - # pyatmo.thermostat._GETHOMESDATA_REQ, - # json=json_fixture, - # headers={"content-type": "application/json"}, - # ) homeStatus = pyatmo.HomeStatus(auth, home_id="91763b24c43d3e344f424e8b") assert len(homeStatus.rooms) == 3 From 52a897ec3399eef134e2222d019c383c2500aeac Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 27 May 2020 12:37:37 +0200 Subject: [PATCH 234/333] Update fixtures --- tests/conftest.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index cf314455a..b23a37ef2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -100,4 +100,25 @@ def cameraHomeData(auth, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) + for index in ["w", "z", "g"]: + vpn_url = ( + f"https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/" + f"6d278460699e56180d47ab47169efb31/" + f"MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTT{index},," + ) + with open("fixtures/camera_ping.json") as f: + json_fixture = json.load(f) + requests_mock.post( + vpn_url + "/command/ping", + json=json_fixture, + headers={"content-type": "application/json"}, + ) + local_url = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" + with open("fixtures/camera_ping.json") as f: + json_fixture = json.load(f) + requests_mock.post( + local_url + "/command/ping", + json=json_fixture, + headers={"content-type": "application/json"}, + ) return pyatmo.CameraData(auth) From 827478a44d69857baa0aac5877b38596146cbdf5 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 28 May 2020 17:54:20 +0200 Subject: [PATCH 235/333] Update camera tests --- fixtures/camera_data_empty.json | 5 + fixtures/camera_data_events_until.json | 48 ++++ fixtures/camera_home_data.json | 56 +++++ fixtures/camera_home_data_no_homes.json | 17 ++ fixtures/camera_image_sample.jpg | Bin 0 -> 671 bytes src/pyatmo/camera.py | 300 ++++++++---------------- tests/test_pyatmo_camera.py | 192 ++++++++++++++- 7 files changed, 414 insertions(+), 204 deletions(-) create mode 100644 fixtures/camera_data_empty.json create mode 100644 fixtures/camera_data_events_until.json create mode 100644 fixtures/camera_home_data_no_homes.json create mode 100644 fixtures/camera_image_sample.jpg diff --git a/fixtures/camera_data_empty.json b/fixtures/camera_data_empty.json new file mode 100644 index 000000000..63c51349f --- /dev/null +++ b/fixtures/camera_data_empty.json @@ -0,0 +1,5 @@ +{ + "status": "ok", + "time_exec": 0.03621506690979, + "time_server": 1560626960 +} \ No newline at end of file diff --git a/fixtures/camera_data_events_until.json b/fixtures/camera_data_events_until.json new file mode 100644 index 000000000..4d7d3efad --- /dev/null +++ b/fixtures/camera_data_events_until.json @@ -0,0 +1,48 @@ +{ + "body": { + "events_list": [ + { + "id": "a1b2c3d4e5f6abcdef123461", + "type": "person", + "time": 1560506232, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "video_status": "deleted", + "is_arrival": true, + "message": "John Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef123462", + "type": "person_away", + "time": 1560506237, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "message": "John Doe hat das Haus verlassen", + "sub_message": "John Doe gilt als „Abwesend“, da das mit diesem Profil verbundene Telefon den Bereich des Hauses verlassen hat." + }, + { + "id": "a1b2c3d4e5f6abcdef123463", + "type": "person", + "time": 1560506241, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "snapshot": { + "id": "19b13efa945ec892c6da2a8c", + "version": 1, + "key": "1704853cfc9571bd10618591dc9035e5bc0fa3203f44739c49a5b26d2f7ad67f", + "url": "https://netatmocameraimage.blob.core.windows.net/production/5ecfa94c6da5e5bc0fa3203f3cfdc903489219b13e2a8c548547b26d2f7ad6717039c49ac9571bd10618591f" + }, + "video_id": "f914-aa7da416643-4744-82f9-4e7d4440b", + "video_status": "available", + "is_arrival": false, + "message": "Jane Doe gesehen" + } + ] + }, + "status": "ok", + "time_exec": 0.03666909215079, + "time_server": 15605062321 +} \ No newline at end of file diff --git a/fixtures/camera_home_data.json b/fixtures/camera_home_data.json index 962c3b85b..ab850b03c 100644 --- a/fixtures/camera_home_data.json +++ b/fixtures/camera_home_data.json @@ -68,6 +68,19 @@ "sd_status": "on", "alim_status": "on", "name": "Hall", + "modules": [ + { + "id": "12:34:56:00:f2:f1", + "type": "NIS", + "battery_percent": 84, + "rf": 68, + "status": "no_news", + "monitoring": "on", + "alim_source": "battery", + "tamper_detection_enabled": true, + "name": "Welcome's Siren" + } + ], "use_pin_code": false, "last_setup": 1544828430 }, @@ -207,6 +220,49 @@ "device_id": "12:34:56:00:8b:a2", "sub_type": 1, "message": "Hall:WLAN-Verbindung erfolgreich hergestellt" + }, + { + "id": "a1b2c3d4e5f6abcdef12345e", + "type": "outdoor", + "time": 1560643100, + "camera_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:00:a5:a4", + "video_id": "string", + "video_status": "available", + "event_list": [ + { + "type": "string", + "time": 1560643100, + "offset": 0, + "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0000", + "message": "Animal dÊtectÊ", + "snapshot": { + "id": "5715e16849c75xxxx00000000xxxxx", + "version": 1, + "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000", + "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa" + }, + "vignette": { + "id": "5715e16849c75xxxx00000000xxxxx", + "version": 1, + "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000", + "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa00000" + } + }, + { + "type": "string", + "time": 1560506222, + "offset": 0, + "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0000", + "message": "Animal dÊtectÊ", + "snapshot": { + "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c53b-aze7a.jpg" + }, + "vignette": { + "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c5.jpg" + } + } + ] } ] }, diff --git a/fixtures/camera_home_data_no_homes.json b/fixtures/camera_home_data_no_homes.json new file mode 100644 index 000000000..78e1450d5 --- /dev/null +++ b/fixtures/camera_home_data_no_homes.json @@ -0,0 +1,17 @@ +{ + "body": { + "homes": [], + "user": { + "reg_locale": "de-DE", + "lang": "de-DE", + "country": "DE", + "mail": "john@doe.com" + }, + "global_info": { + "show_tags": true + } + }, + "status": "ok", + "time_exec": 0.03621506690979, + "time_server": 1560626960 +} \ No newline at end of file diff --git a/fixtures/camera_image_sample.jpg b/fixtures/camera_image_sample.jpg new file mode 100644 index 0000000000000000000000000000000000000000..40dcd0914ba73111e33ac49d2eed0a80d7b2c039 GIT binary patch literal 671 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_kjh()c^7mY&dJpZn>z?BcD(&onkwh_3%A dF#m_&R=@hE8~-`&3{SgvXWsI?4E6tS0s!~g None: vpn_url = camera_data.get("vpn_url") if camera_data.get("is_local"): - def check_url(url): + def check_url(url: str) -> str: if url is None: return None try: @@ -190,8 +190,8 @@ def set_persons_away(self, person_id, home_id): resp = self.authData.post_request(url=_SETPERSONSAWAY_REQ, params=postParams) return resp - def get_person_id(self, name): - """Retrieve the ID of a person + def get_person_id(self, name: str) -> str: + """Retrieve the ID of a person. Arguments: name {str} -- Name of a person @@ -204,10 +204,8 @@ def get_person_id(self, name): return pid return None - def getCameraPicture(self, image_id, key): - """ - Download a specific image (of an event or user face) from the camera - """ + def get_camera_picture(self, image_id: str, key: str): + """Download a specific image (of an event or user face) from the camera.""" postParams = { "image_id": image_id, "key": key, @@ -216,68 +214,64 @@ def getCameraPicture(self, image_id, key): image_type = imghdr.what("NONE.FILE", resp) return resp, image_type - def getProfileImage(self, name): - """ - Retrieve the face of a given person - """ + def get_profile_image(self, name: str): + """Retrieve the face of a given person.""" for p in self.persons: if "pseudo" in self.persons[p]: if name == self.persons[p]["pseudo"]: image_id = self.persons[p]["face"]["id"] key = self.persons[p]["face"]["key"] - return self.getCameraPicture(image_id, key) + return self.get_camera_picture(image_id, key) + return None, None - def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): - """ - Update the list of events - """ - if not home_id: - try: - if not home: - home = self.default_home - home_id = self.gethomeId(home) - except InvalidHome: - LOG.debug("Invalid Home %s", home) - return None - - if devicetype == "NACamera": + def update_events( + self, home_id: str, event_id: str = None, device_type: str = None + ) -> None: + """Update the list of events.""" + # Either event_id or device_type must be given + if not event_id and not device_type: + raise ApiError + + if device_type == "NACamera": # for the Welcome camera - if not event: + if not event_id: # If no event is provided we need to retrieve the oldest of # the last event seen by each camera - listEvent = {} - for cam_id in self.lastEvent: - listEvent[self.lastEvent[cam_id]["time"]] = self.lastEvent[cam_id] - event = listEvent[sorted(listEvent)[0]] - - if devicetype == "NOC": + event_list = {} + for cam_id in self.last_event: + event_list[self.last_event[cam_id]["time"]] = self.last_event[ + cam_id + ] + event_id = event_list[sorted(event_list)[0]]["id"] + + if device_type == "NOC": # for the Presence camera - if not event: + if not event_id: # If no event is provided we need to retrieve the oldest of # the last event seen by each camera - listEvent = {} - for cam_id in self.outdoor_lastEvent: - listEvent[ - self.outdoor_lastEvent[cam_id]["time"] - ] = self.outdoor_lastEvent[cam_id] - event = listEvent[sorted(listEvent)[0]] - - if devicetype == "NSD": + event_list = {} + for cam_id in self.outdoor_last_event: + event_list[ + self.outdoor_last_event[cam_id]["time"] + ] = self.outdoor_last_event[cam_id] + event_id = event_list[sorted(event_list)[0]]["id"] + + if device_type == "NSD": # for the smoke detector - if not event: + if not event_id: # If no event is provided we need to retrieve the oldest of # the last event by each smoke detector - listEvent = {} - for sid in self.outdoor_lastEvent: - listEvent[ - self.outdoor_lastEvent[sid]["time"] - ] = self.outdoor_lastEvent[sid] - event = listEvent[sorted(listEvent)[0]] + event_list = {} + for sid in self.outdoor_last_event: + event_list[ + self.outdoor_last_event[sid]["time"] + ] = self.outdoor_last_event[sid] + event_id = event_list[sorted(event_list)[0]]["id"] postParams = { "home_id": home_id, - "event_id": event["id"], + "event_id": event_id, } eventList = [] @@ -302,11 +296,11 @@ def updateEvent(self, event=None, home=None, devicetype=None, home_id=None): self.events[e["camera_id"]] = {} self.events[e["camera_id"]][e["time"]] = e for camera in self.events: - self.lastEvent[camera] = self.events[camera][ + self.last_event[camera] = self.events[camera][ sorted(self.events[camera])[-1] ] for camera in self.outdoor_events: - self.outdoor_lastEvent[camera] = self.outdoor_events[camera][ + self.outdoor_last_event[camera] = self.outdoor_events[camera][ sorted(self.outdoor_events[camera])[-1] ] @@ -326,26 +320,26 @@ def person_seen_by_camera(self, name, cid, exclude=0): if "pseudo" in self.persons[person_id]: if self.persons[person_id]["pseudo"] == name: return True - elif self.lastEvent[cid]["type"] == "person": - person_id = self.lastEvent[cid]["person_id"] + elif self.last_event[cid]["type"] == "person": + person_id = self.last_event[cid]["person_id"] if "pseudo" in self.persons[person_id]: if self.persons[person_id]["pseudo"] == name: return True return False - def _knownPersons(self): + def _known_persons(self): known_persons = {} for p_id, p in self.persons.items(): if "pseudo" in p: known_persons[p_id] = p return known_persons - def knownPersons(self): - return {pid: p["pseudo"] for pid, p in self._knownPersons().items()} + def known_persons(self): + return {pid: p["pseudo"] for pid, p in self._known_persons().items()} - def knownPersonsNames(self): + def known_persons_names(self): names = [] - for _, p in self._knownPersons().items(): + for _, p in self._known_persons().items(): names.append(p["pseudo"]) return names @@ -360,11 +354,11 @@ def someone_known_seen(self, cid: str, exclude: int = 0) -> bool: if time_ev < limit: return False if self.events[cid][time_ev]["type"] == "person": - if self.events[cid][time_ev]["person_id"] in self._knownPersons(): + if self.events[cid][time_ev]["person_id"] in self._known_persons(): return True # Check in the last event if someone known has been seen - elif self.lastEvent[cid]["type"] == "person": - if self.lastEvent[cid]["person_id"] in self._knownPersons(): + elif self.last_event[cid]["type"] == "person": + if self.last_event[cid]["person_id"] in self._known_persons(): return True return False @@ -381,12 +375,12 @@ def someone_unknown_seen(self, cid: str, exclude: int = 0) -> bool: if self.events[cid][time_ev]["type"] == "person": if ( self.events[cid][time_ev]["person_id"] - not in self._knownPersons() + not in self._known_persons() ): return True # Check in the last event is someone known has been seen - elif self.lastEvent[cid]["type"] == "person": - if self.lastEvent[cid]["person_id"] not in self._knownPersons(): + elif self.last_event[cid]["type"] == "person": + if self.last_event[cid]["person_id"] not in self._known_persons(): return True return False @@ -402,129 +396,51 @@ def motion_detected(self, cid, exclude=0): return False if self.events[cid][time_ev]["type"] == "movement": return True - elif self.lastEvent[cid]["type"] == "movement": + elif self.last_event[cid]["type"] == "movement": return True return False - def outdoormotionDetected(self, home=None, camera=None, offset=0, cid=None): - """ - Evaluate if outdoor movement has been detected - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("outdoormotionDetected: Camera name or home is unknown") - return False - return self.outdoor_motion_detected(cid=cid, offset=0) - - def outdoor_motion_detected(self, cid, offset=0): - """ - Evaluate if outdoor movement has been detected - """ - if cid in self.lastEvent: - if self.lastEvent[cid]["type"] == "movement": - if self.lastEvent[cid][ + def outdoor_motion_detected(self, cid: str, offset: int = 0) -> bool: + """Evaluate if outdoor movement has been detected.""" + if cid in self.last_event: + if self.last_event[cid]["type"] == "movement": + if self.last_event[cid][ "video_status" - ] == "recording" and self.lastEvent[cid]["time"] + offset > int( + ] == "recording" and self.last_event[cid]["time"] + offset > int( time.time() ): return True return False - def humanDetected(self, home=None, camera=None, offset=0, cid=None): - """ - Evaluate if a human has been detected - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("personSeenByCamera: Camera name or home is unknown") - return False - return self.human_detected(cid=cid, offset=0) - - def human_detected(self, cid, offset=0): - """ - Evaluate if a human has been detected - """ - if self.outdoor_lastEvent[cid]["video_status"] == "recording": - for e in self.outdoor_lastEvent[cid]["event_list"]: + def human_detected(self, cid: str, offset: int = 0) -> bool: + """Evaluate if a human has been detected.""" + if self.outdoor_last_event[cid]["video_status"] == "recording": + for e in self.outdoor_last_event[cid]["event_list"]: if e["type"] == "human" and e["time"] + offset > int(time.time()): return True return False - def animalDetected(self, home=None, camera=None, offset=0, cid=None): - """ - Evaluate if an animal has been detected - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("animalDetected: Camera name or home is unknown") - return False - return self.animal_detected(cid=cid, offset=0) - - def animal_detected(self, cid, offset=0): - """ - Evaluate if an animal has been detected - """ - if self.outdoor_lastEvent[cid]["video_status"] == "recording": - for e in self.outdoor_lastEvent[cid]["event_list"]: + def animal_detected(self, cid: str, offset: int = 0) -> bool: + """Evaluate if an animal has been detected.""" + if self.outdoor_last_event[cid]["video_status"] == "recording": + for e in self.outdoor_last_event[cid]["event_list"]: if e["type"] == "animal" and e["time"] + offset > int(time.time()): return True return False - def carDetected(self, home=None, camera=None, offset=0, cid=None): - """ - Evaluate if a car has been detected - (old interface) - """ - if not cid: - try: - cid = self.cameraByName(camera=camera, home=home)["id"] - except TypeError: - LOG.error("carDetected: Camera name or home is unknown") - return False - return self.car_detected(cid=cid, offset=offset) - - def car_detected(self, cid, offset=0): - """ - Evaluate if a car has been detected - """ - if self.outdoor_lastEvent[cid]["video_status"] == "recording": - for e in self.outdoor_lastEvent[cid]["event_list"]: + def car_detected(self, cid: str, offset: int = 0) -> bool: + """Evaluate if a car has been detected.""" + if self.outdoor_last_event[cid]["video_status"] == "recording": + for e in self.outdoor_last_event[cid]["event_list"]: if e["type"] == "vehicle" and e["time"] + offset > int(time.time()): return True return False - def moduleMotionDetected(self, module=None, home=None, camera=None, exclude=0): - """ - Evaluate if movement has been detected - (old interface) - """ - try: - mod = self.moduleByName(module, camera=camera, home=home) - mid = mod["id"] - cid = mod["cam_id"] - except TypeError: - LOG.error( - "moduleMotionDetected: Module name or Camera name or home is unknown" - ) - return False - return self.module_motion_detected(mid=mid, cid=cid, exclude=exclude) - - def module_motion_detected(self, mid, cid, exclude=0): - """ - Evaluate if movement has been detected - """ + def module_motion_detected(self, mid: str, cid: str, exclude: int = 0) -> bool: + """Evaluate if movement has been detected.""" if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cid], reverse=True) + array_time_event = sorted(self.events.get(cid, []), reverse=True) for time_ev in array_time_event: if time_ev < limit: return False @@ -534,29 +450,21 @@ def module_motion_detected(self, mid, cid, exclude=0): ) and self.events[cid][time_ev]["module_id"] == mid: return True elif ( - self.lastEvent[cid]["type"] == "tag_big_move" - or self.lastEvent[cid]["type"] == "tag_small_move" - ) and self.lastEvent[cid]["module_id"] == mid: + cid in self.last_event + and ( + self.last_event[cid]["type"] == "tag_big_move" + or self.last_event[cid]["type"] == "tag_small_move" + ) + and self.last_event[cid]["module_id"] == mid + ): return True return False - def moduleOpened(self, module=None, home=None, camera=None, exclude=0): - """ - Evaluate if module status is open - """ - try: - mod = self.moduleByName(module, camera=camera, home=home) - mid = mod["id"] - cid = mod["cam_id"] - except TypeError: - LOG.error("moduleOpened: Camera name, or home, or module is unknown") - return False - return self.module_opened(mid=mid, cid=cid, exclude=exclude) - - def module_opened(self, mid, cid, exclude=0): + def module_opened(self, mid: str, cid: str, exclude: int = 0) -> bool: + """Evaluate if module status is open.""" if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cid], reverse=True) + array_time_event = sorted(self.events.get(cid, []), reverse=True) for time_ev in array_time_event: if time_ev < limit: return False @@ -565,9 +473,9 @@ def module_opened(self, mid, cid, exclude=0): and self.events[cid][time_ev]["module_id"] == mid ): return True - elif ( - self.lastEvent[cid]["type"] == "tag_open" - and self.lastEvent[cid]["module_id"] == mid + elif cid in self.last_event and ( + self.last_event[cid]["type"] == "tag_open" + and self.last_event[cid]["module_id"] == mid ): return True return False diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 4c3df7946..b5ec790a8 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -1,5 +1,4 @@ """Define tests for Camera module.""" -from contextlib import nullcontext as does_not_raise import json import pytest @@ -7,13 +6,37 @@ import pyatmo -INVALID_NAME = "InvalidName" +from .conftest import does_not_raise def test_CameraData(cameraHomeData): assert cameraHomeData.homes is not None +def test_HomeData_no_body(auth, requests_mock): + with open("fixtures/camera_data_empty.json") as f: + json_fixture = json.load(f) + requests_mock.post( + pyatmo.camera._GETHOMEDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with pytest.raises(pyatmo.NoDevice): + assert pyatmo.CameraData(auth) + + +def test_HomeData_no_homes(auth, requests_mock): + with open("fixtures/camera_home_data_no_homes.json") as f: + json_fixture = json.load(f) + requests_mock.post( + pyatmo.camera._GETHOMEDATA_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with pytest.raises(pyatmo.NoDevice): + assert pyatmo.CameraData(auth) + + @pytest.mark.parametrize( "cid, expected", [ @@ -57,10 +80,58 @@ def test_CameraData_camera_urls(cameraHomeData, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) + cameraHomeData.update_camera_urls(cid) + assert cameraHomeData.camera_urls(cid) == (vpn_url, local_url) +def test_CameraData_update_camera_urls_empty(cameraHomeData): + camera_id = "12:34:56:00:f1:62" + home_id = "91763b24c43d3e344f424e8b" + cameraHomeData.cameras[home_id][camera_id]["vpn_url"] = None + cameraHomeData.cameras[home_id][camera_id]["local_url"] = None + + cameraHomeData.update_camera_urls(camera_id) + + assert cameraHomeData.camera_urls(camera_id) == (None, None) + + +# def test_CameraData_update_camera_urls_timeout(auth, requests_mock): +# fake_url = "mock://test.com/6" +# camera_id = "12:34:56:00:f1:62" +# home_id = "91763b24c43d3e344f424e8b" + +# with open("fixtures/camera_home_data.json") as f: +# json_fixture = json.load(f) +# requests_mock.post( +# pyatmo.camera._GETHOMEDATA_REQ, +# json=json_fixture, +# headers={"content-type": "application/json"}, +# ) +# for index in ["w", "z", "g"]: +# vpn_url = ( +# f"https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/" +# f"6d278460699e56180d47ab47169efb31/" +# f"MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTT{index},," +# ) +# requests_mock.post(vpn_url + "/command/ping", exc=pyatmo.exceptions.ApiError) +# local_url = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" +# requests_mock.post(local_url + "/command/ping", exc=pyatmo.exceptions.ApiError) + +# with pytest.raises(pyatmo.exceptions.ApiError): +# cameraHomeData = pyatmo.CameraData(auth) +# cameraHomeData.cameras[home_id][camera_id]["vpn_url"] = fake_url +# cameraHomeData.cameras[home_id][camera_id]["local_url"] = fake_url + +# requests_mock.post(fake_url + "/command/ping", exc=pyatmo.exceptions.ApiError) + +# with pytest.raises(pyatmo.exceptions.ApiError): +# cameraHomeData.update_camera_urls(camera_id) + +# assert cameraHomeData.camera_urls(camera_id) == (fake_url, None) + + def test_CameraData_camera_urls_disconnected(auth, requests_mock): with open("fixtures/camera_home_data_disconnected.json") as f: json_fixture = json.load(f) @@ -119,14 +190,20 @@ def test_CameraData_person_seen_by_camera(cameraHomeData, name, cid, exclude, ex assert cameraHomeData.person_seen_by_camera(name, cid, exclude=exclude) is expected -def test_CameraData__knownPersons(cameraHomeData): - knownPersons = cameraHomeData._knownPersons() - assert len(knownPersons) == 3 - assert knownPersons["91827374-7e04-5298-83ad-a0cb8372dff1"]["pseudo"] == "John Doe" +def test_CameraData__known_persons(cameraHomeData): + known_persons = cameraHomeData._known_persons() + assert len(known_persons) == 3 + assert known_persons["91827374-7e04-5298-83ad-a0cb8372dff1"]["pseudo"] == "John Doe" + + +def test_CameraData_known_persons(cameraHomeData): + known_persons = cameraHomeData.known_persons() + assert len(known_persons) == 3 + assert known_persons["91827374-7e04-5298-83ad-a0cb8372dff1"] == "John Doe" -def test_CameraData_knownPersonsNames(cameraHomeData): - assert sorted(cameraHomeData.knownPersonsNames()) == [ +def test_CameraData_known_persons_names(cameraHomeData): + assert sorted(cameraHomeData.known_persons_names()) == [ "Jane Doe", "John Doe", "Richard Doe", @@ -139,6 +216,7 @@ def test_CameraData_knownPersonsNames(cameraHomeData): [ ("John Doe", "91827374-7e04-5298-83ad-a0cb8372dff1"), ("Richard Doe", "91827376-7e04-5298-83af-a0cb8372dff3"), + ("Dexter Foe", None), ], ) def test_CameraData_get_person_id(cameraHomeData, name, expected): @@ -346,3 +424,101 @@ def test_CameraData_set_state( ) == expected ) + + +def test_CameraData_get_light_state(cameraHomeData): + camera_id = "12:34:56:00:a5:a4" + expected = "auto" + assert cameraHomeData.get_light_state(camera_id) == expected + + +def test_CameraData_get_camera_picture(cameraHomeData, requests_mock): + image_id = "5c22739723720a6e278c43bf" + key = "276751836a6d1a71447f8d975494c87bc125766a970f7e022e79e001e021d756" + with open("fixtures/camera_image_sample.jpg", "rb") as f: + expect = f.read() + + requests_mock.post(pyatmo.camera._GETCAMERAPICTURE_REQ, content=expect) + + assert cameraHomeData.get_camera_picture(image_id, key) == (expect, "jpeg") + + +def test_CameraData_get_profile_image(cameraHomeData, requests_mock): + with open("fixtures/camera_image_sample.jpg", "rb") as f: + expect = f.read() + + requests_mock.post(pyatmo.camera._GETCAMERAPICTURE_REQ, content=expect) + assert cameraHomeData.get_profile_image("John Doe") == (expect, "jpeg") + assert cameraHomeData.get_profile_image("Jack Foe") == (None, None) + + +@pytest.mark.parametrize( + "home_id, event_id, device_type, exception", + [ + ("91763b24c43d3e344f424e8b", None, None, pytest.raises(pyatmo.ApiError)), + ( + "91763b24c43d3e344f424e8b", + "a1b2c3d4e5f6abcdef123456", + None, + does_not_raise(), + ), + ("91763b24c43d3e344f424e8b", None, "NOC", does_not_raise()), + ("91763b24c43d3e344f424e8b", None, "NACamera", does_not_raise()), + ("91763b24c43d3e344f424e8b", None, "NSD", does_not_raise()), + ], +) +def test_CameraData_update_event( + cameraHomeData, requests_mock, home_id, event_id, device_type, exception +): + with open("fixtures/camera_data_events_until.json") as f: + json_fixture = json.load(f) + requests_mock.post( + pyatmo.camera._GETEVENTSUNTIL_REQ, + json=json_fixture, + headers={"content-type": "application/json"}, + ) + with exception: + assert ( + cameraHomeData.update_events( + home_id=home_id, event_id=event_id, device_type=device_type + ) + is None + ) + + +def test_CameraData_outdoor_motion_detected(cameraHomeData): + camera_id = "12:34:56:00:a5:a4" + assert cameraHomeData.outdoor_motion_detected(camera_id) is False + assert cameraHomeData.outdoor_motion_detected(camera_id, 100) is False + + +def test_CameraData_human_detected(cameraHomeData): + camera_id = "12:34:56:00:a5:a4" + assert cameraHomeData.human_detected(camera_id) is False + assert cameraHomeData.human_detected(camera_id, 100) is False + + +def test_CameraData_animal_detected(cameraHomeData): + camera_id = "12:34:56:00:a5:a4" + assert cameraHomeData.animal_detected(camera_id) is False + assert cameraHomeData.animal_detected(camera_id, 100) is False + + +def test_CameraData_car_detected(cameraHomeData): + camera_id = "12:34:56:00:a5:a4" + assert cameraHomeData.car_detected(camera_id) is False + assert cameraHomeData.car_detected(camera_id, 100) is False + + +def test_CameraData_module_motion_detected(cameraHomeData): + camera_id = "12:34:56:00:f1:62" + module_id = "12:34:56:00:f2:f1" + assert cameraHomeData.module_motion_detected(camera_id, module_id) is False + assert cameraHomeData.module_motion_detected(camera_id, module_id, 100) is False + + +def test_CameraData_module_opened(cameraHomeData): + camera_id = "12:34:56:00:f1:62" + module_id = "12:34:56:00:f2:f1" + assert cameraHomeData.module_opened(camera_id, module_id) is False + assert cameraHomeData.module_opened(camera_id, module_id, 100) is False From 2500d78a7d3c9dc673570ab72648a91e6aaf04a6 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 28 May 2020 18:02:20 +0200 Subject: [PATCH 236/333] Fix variable names --- src/pyatmo/camera.py | 88 ++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index d46cb9351..7fbcf6e7f 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -20,23 +20,23 @@ class CameraData: List of Netatmo camera informations (Homes, cameras, smoke detectors, modules, events, persons) Args: - authData (ClientAuth): + auth_data (ClientAuth): Authentication information with a valid access token """ - def __init__(self, authData, size=15): - self.authData = authData + def __init__(self, auth_data, size=15): + self.auth_data = auth_data - postParams = {"size": size} - resp = self.authData.post_request(url=_GETHOMEDATA_REQ, params=postParams) + post_params = {"size": size} + resp = self.auth_data.post_request(url=_GETHOMEDATA_REQ, params=post_params) if resp is None or "body" not in resp: raise NoDevice("No device data returned by Netatmo server") - self.rawData = resp["body"].get("homes") - if not self.rawData: + self.raw_data = resp["body"].get("homes") + if not self.raw_data: raise NoDevice("No device data available") - self.homes = {d["id"]: d for d in self.rawData} + self.homes = {d["id"]: d for d in self.raw_data} self.persons = {} self.events = {} @@ -48,18 +48,18 @@ def __init__(self, authData, size=15): self.outdoor_last_event = {} self.types = {} - for item in self.rawData: - homeId = item.get("id") - nameHome = item.get("name") - if not nameHome: - nameHome = "Unknown" - self.homes[homeId]["name"] = nameHome - if homeId not in self.cameras: - self.cameras[homeId] = {} - if homeId not in self.smokedetectors: - self.smokedetectors[homeId] = {} - if homeId not in self.types: - self.types[homeId] = {} + for item in self.raw_data: + home_id = item.get("id") + home_name = item.get("name") + if not home_name: + home_name = "Unknown" + self.homes[home_id]["name"] = home_name + if home_id not in self.cameras: + self.cameras[home_id] = {} + if home_id not in self.smokedetectors: + self.smokedetectors[home_id] = {} + if home_id not in self.types: + self.types[home_id] = {} for p in item["persons"]: self.persons[p["id"]] = p if "events" in item: @@ -73,18 +73,18 @@ def __init__(self, authData, size=15): self.events[e["camera_id"]] = {} self.events[e["camera_id"]][e["time"]] = e for c in item["cameras"]: - self.cameras[homeId][c["id"]] = c - self.cameras[homeId][c["id"]]["home_id"] = homeId + self.cameras[home_id][c["id"]] = c + self.cameras[home_id][c["id"]]["home_id"] = home_id if c["type"] == "NACamera" and "modules" in c: for m in c["modules"]: self.modules[m["id"]] = m self.modules[m["id"]]["cam_id"] = c["id"] for s in item["smokedetectors"]: - self.smokedetectors[homeId][s["id"]] = s + self.smokedetectors[home_id][s["id"]] = s for t in item["cameras"]: - self.types[homeId][t["type"]] = t + self.types[home_id][t["type"]] = t for t in item["smokedetectors"]: - self.types[homeId][t["type"]] = t + self.types[home_id][t["type"]] = t for camera in self.events: self.last_event[camera] = self.events[camera][ sorted(self.events[camera])[-1] @@ -119,7 +119,7 @@ def get_smokedetector(self, sid: str): def camera_urls(self, cid: str) -> Tuple[str, str]: """ Return the vpn_url and the local_url (if available) of a given camera - in order to access its live feed + in order to access its live feed. """ camera_data = self.get_camera(cid) return camera_data.get("vpn_url", None), camera_data.get("local_url", None) @@ -137,7 +137,7 @@ def check_url(url: str) -> str: if url is None: return None try: - resp = self.authData.post_request(url=f"{url}/command/ping") + resp = self.auth_data.post_request(url=f"{url}/command/ping") except (ApiError, ReadTimeout): LOG.debug("Timeout validation the camera url %s", url) return None @@ -166,11 +166,11 @@ def set_persons_home(self, person_ids, home_id): """ Mark persons as home. """ - postParams = { + post_params = { "home_id": home_id, "person_ids[]": person_ids, } - resp = self.authData.post_request(url=_SETPERSONSHOME_REQ, params=postParams) + resp = self.auth_data.post_request(url=_SETPERSONSHOME_REQ, params=post_params) return resp def set_persons_away(self, person_id, home_id): @@ -183,11 +183,11 @@ def set_persons_away(self, person_id, home_id): Returns: [type] -- [description] """ - postParams = { + post_params = { "home_id": home_id, "person_id": person_id, } - resp = self.authData.post_request(url=_SETPERSONSAWAY_REQ, params=postParams) + resp = self.auth_data.post_request(url=_SETPERSONSAWAY_REQ, params=post_params) return resp def get_person_id(self, name: str) -> str: @@ -206,11 +206,13 @@ def get_person_id(self, name: str) -> str: def get_camera_picture(self, image_id: str, key: str): """Download a specific image (of an event or user face) from the camera.""" - postParams = { + post_params = { "image_id": image_id, "key": key, } - resp = self.authData.post_request(url=_GETCAMERAPICTURE_REQ, params=postParams) + resp = self.auth_data.post_request( + url=_GETCAMERAPICTURE_REQ, params=post_params + ) image_type = imghdr.what("NONE.FILE", resp) return resp, image_type @@ -269,24 +271,24 @@ def update_events( ] = self.outdoor_last_event[sid] event_id = event_list[sorted(event_list)[0]]["id"] - postParams = { + post_params = { "home_id": home_id, "event_id": event_id, } - eventList = [] + event_list = [] try: - resp = self.authData.post_request( - url=_GETEVENTSUNTIL_REQ, params=postParams + resp = self.auth_data.post_request( + url=_GETEVENTSUNTIL_REQ, params=post_params ) - eventList = resp["body"]["events_list"] + event_list = resp["body"]["events_list"] except ApiError: pass except KeyError: - LOG.debug("eventList response: %s", resp) - LOG.debug("eventList body: %s", resp["body"]) + LOG.debug("event_list response: %s", resp) + LOG.debug("event_list body: %s", resp["body"]) - for e in eventList: + for e in event_list: if e["type"] == "outdoor": if e["camera_id"] not in self.outdoor_events: self.outdoor_events[e["camera_id"]] = {} @@ -517,12 +519,12 @@ def set_state( else: module[param] = val - postParams = { + post_params = { "json": {"home": {"id": home_id, "modules": [module]}}, } try: - resp = self.authData.post_request(url=_SETSTATE_REQ, params=postParams) + resp = self.auth_data.post_request(url=_SETSTATE_REQ, params=post_params) except ApiError as err_msg: LOG.error("%s", err_msg) return False From fdfdb8d22b19c4682df75fe0df5c3d91c99db830 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 29 May 2020 22:30:36 +0200 Subject: [PATCH 237/333] Refactor weatherstation and home coach --- src/pyatmo/home_coach.py | 6 +- src/pyatmo/weather_station.py | 325 +++++++++--------- tests/test_pyatmo_homecoach.py | 48 +-- tests/test_pyatmo_weatherstation.py | 497 ++++++++-------------------- 4 files changed, 291 insertions(+), 585 deletions(-) diff --git a/src/pyatmo/home_coach.py b/src/pyatmo/home_coach.py index 21e11180d..d66d5871e 100644 --- a/src/pyatmo/home_coach.py +++ b/src/pyatmo/home_coach.py @@ -8,8 +8,8 @@ class HomeCoachData(WeatherStationData): """ List the Home Couch devices (stations and modules) Args: - authData (ClientAuth): Authentication information with a working access Token + auth_data (ClientAuth): Authentication information with a working access Token """ - def __init__(self, authData): - super(HomeCoachData, self).__init__(authData, urlReq=_GETHOMECOACHDATA_REQ) + def __init__(self, auth_data): + super(HomeCoachData, self).__init__(auth_data, url_req=_GETHOMECOACHDATA_REQ) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 1fab6a07c..f8aa544f1 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -1,5 +1,6 @@ import logging import time +from typing import Dict, List from .exceptions import NoDevice from .helpers import _BASE_URL, fixId, todayStamps @@ -14,14 +15,16 @@ class WeatherStationData: """ List the Weather Station devices (stations and modules) Args: - authData (ClientAuth): Authentication information with a working access Token + auth_data (ClientAuth): Authentication information with a working access Token """ - def __init__(self, authData, urlReq=None): + def __init__(self, auth_data, url_req: str = None): """Initialize the weather station class.""" - self.urlReq = urlReq or _GETSTATIONDATA_REQ - self.authData = authData - resp = self.authData.post_request(url=self.urlReq) + self.url_req = url_req or _GETSTATIONDATA_REQ + self.auth_data = auth_data + + resp = self.auth_data.post_request(url=self.url_req) + if resp is None or "body" not in resp: raise NoDevice("No weather station data returned by Netatmo server") try: @@ -29,148 +32,114 @@ def __init__(self, authData, urlReq=None): except KeyError: LOG.debug("No in response %s", resp) raise NoDevice("No weather station data returned by Netatmo server") + if not self.rawData: raise NoDevice("No weather station available") + self.stations = {d["_id"]: d for d in self.rawData} self.modules = {} + for item in self.rawData: + if "modules" not in item: item["modules"] = [item] + for m in item["modules"]: if "module_name" not in m: if m["type"] == "NHC": m["module_name"] = m["station_name"] else: continue + self.modules[m["_id"]] = m self.modules[m["_id"]]["main_device"] = item["_id"] + self.default_station = list(self.stations.values())[0]["station_name"] - def modulesNamesList(self, station=None, station_id=None): - """Return a list of all modules for a given or all stations.""" + def get_module_names(self, station_id: str) -> List: + """Return a list of all module names for a given or all stations.""" res = set() - station_data = None - if station_id is not None: - station_data = self.stationById(station_id) - elif station is not None: - station_data = self.stationByName(station) - if station_data is not None: - res.add(station_data.get("module_name", station_data.get("type"))) - for m in station_data["modules"]: - res.add(m.get("module_name", m.get("type"))) - else: - res.update([m["module_name"] for m in self.modules.values()]) - for s in self.stations.values(): - res.add(s.get("module_name", s.get("type"))) + station_data = self.get_station(station_id) + print(station_data) + + if not station_data: + return [] + + res.add(station_data.get("module_name", station_data.get("type"))) + for m in station_data["modules"]: + # Add module name, use module type if no name is available + res.add(m.get("module_name", m.get("type"))) + return list(res) - def getModules(self, station=None, station_id=None): - """Return a dict or modules for a given or all stations.""" + def get_modules(self, station_id: str) -> Dict: + """Return a dict of modules for a given or all stations.""" + station_data = self.get_station(station_id) + + if not station_data: + return {} + res = {} - station_data = None - if station_id is not None: - station_data = self.stationById(station_id) - elif station is not None: - station_data = self.stationByName(station) - if station_data is not None: - stations = [self.stations[station_data["_id"]]] - else: - stations = self.stations.values() - for s in stations: - res[s["_id"]] = { - "station_name": s["station_name"], - "module_name": s.get("module_name", s.get("type")), - "id": s["_id"], + for station in [self.stations[station_data["_id"]]]: + res[station["_id"]] = { + "station_name": station["station_name"], + "module_name": station.get("module_name", station.get("type")), + "id": station["_id"], } - for m in s["modules"]: - res[m["_id"]] = { - "station_name": m.get("station_name", s["station_name"]), - "module_name": m.get("module_name", m.get("type")), - "id": m["_id"], + for module in station["modules"]: + res[module["_id"]] = { + "station_name": module.get("station_name", station["station_name"]), + "module_name": module.get("module_name", module.get("type")), + "id": module["_id"], } return res - def stationByName(self, station=None): - """Return station by name.""" - 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 stationById(self, sid): + def get_station(self, station_id: str) -> Dict: """Return station by id.""" - return None if sid not in self.stations else self.stations[sid] - - def moduleByName(self, module_name, station=None): - """Return module by name.""" - s = None - if station: - s = self.stationByName(station) - if not s: - return None - if s["module_name"] == module_name: - return s - else: - for s in self.stations.values(): - if "module_name" in s: - if s["module_name"] == module_name: - return s - break - for m in self.modules: - module = self.modules[m] - if module["module_name"] == module_name: - if not s or module["main_device"] == s["_id"]: - return module - return None - - def moduleById(self, mid, sid=None): + return self.stations.get(station_id, {}) + + def get_module(self, mid, sid=None): """Return module by id.""" - s = self.stationById(sid) if sid else None + station = self.get_station(sid) if sid else None if mid in self.modules: - if s: - for module in s["modules"]: + if station: + for module in station["modules"]: if module["_id"] == mid: return module else: return self.modules[mid] + return {} - def monitoredConditions(self, module=None, moduleId=None): + def get_monitored_conditions(self, module_id: str) -> List: """Return monitored conditions for given module(s).""" - if moduleId: - mod = self.moduleById(moduleId) - if not mod: - mod = self.stationById(moduleId) - elif module: - mod = self.moduleByName(module) - if not mod: - mod = self.stationByName(module) - else: - return None + module = self.get_module(module_id) + if not module: + module = self.get_station(module_id) + + if not module: + return [] + conditions = [] - if not mod: - return conditions - for cond in mod.get("data_type", []): - if cond == "Wind": + for condition in module.get("data_type", []): + if condition == "Wind": # the Wind meter actually exposes the following conditions conditions.extend( ["WindAngle", "WindStrength", "GustAngle", "GustStrength"] ) - elif cond == "Rain": + elif condition == "Rain": conditions.extend(["Rain", "sum_rain_24", "sum_rain_1"]) else: - conditions.append(cond) - if mod["type"] in ["NAMain", "NHC"]: + conditions.append(condition) + if module["type"] in ["NAMain", "NHC"]: # the main module has wifi_status conditions.append("wifi_status") else: # assume all other modules have rf_status, battery_vp, and battery_percent conditions.extend(["rf_status", "battery_vp", "battery_percent"]) - if mod["type"] in ["NAMain", "NAModule1", "NAModule4", "NHC"]: + if module["type"] in ["NAMain", "NAModule1", "NAModule4", "NHC"]: conditions.extend(["min_temp", "max_temp"]) - if mod["type"] in [ + if module["type"] in [ "NAMain", "NAModule1", "NAModule2", @@ -181,68 +150,70 @@ def monitoredConditions(self, module=None, moduleId=None): conditions.append("reachable") return conditions - def lastData(self, station=None, exclude=0, byId=False): + def get_last_data(self, station_id=None, exclude=0): """Return data for a given station and time frame.""" - key = "_id" if byId else "module_name" - if station is not None: - stations = [station] - elif byId: - stations = [s["_id"] for s in list(self.stations.values())] - else: - stations = [s["station_name"] for s in list(self.stations.values())] + key = "_id" + # Breaking change from Netatmo : dashboard_data no longer available if station lost lastD = {} - for st in stations: - s = self.stationById(st) if byId else self.stationByName(st) - if not s or "dashboard_data" not in s: - LOG.info("No dashboard data for station %s", st) + s = self.get_station(station_id) + + if not s or "dashboard_data" not in s: + LOG.debug("No dashboard data for station %s", station_id) + return lastD + + # Define oldest acceptable sensor measure event + limit = (time.time() - exclude) if exclude else 0 + + ds = s["dashboard_data"] + if key in s and ds["time_utc"] > limit: + lastD[s[key]] = ds.copy() + lastD[s[key]]["When"] = lastD[s[key]].pop("time_utc") + lastD[s[key]]["wifi_status"] = s["wifi_status"] + lastD[s[key]]["reachable"] = s["reachable"] + + for module in s["modules"]: + + if "dashboard_data" not in module or key not in module: continue - # Define oldest acceptable sensor measure event - limit = (time.time() - exclude) if exclude else 0 - ds = s["dashboard_data"] - if key in s and ds["time_utc"] > limit: - lastD[s[key]] = ds.copy() - lastD[s[key]]["When"] = lastD[s[key]].pop("time_utc") - lastD[s[key]]["wifi_status"] = s["wifi_status"] - lastD[s[key]]["reachable"] = s["reachable"] - for module in s["modules"]: - if "dashboard_data" not in module or key not in module: - continue - ds = module["dashboard_data"] - if "time_utc" in ds and ds["time_utc"] > limit: - lastD[module[key]] = ds.copy() - lastD[module[key]]["When"] = lastD[module[key]].pop("time_utc") - # For potential use, add battery and radio coverage information to module data if present - for i in ( - "rf_status", - "battery_vp", - "battery_percent", - "reachable", - "wifi_status", - ): - if i in module: - lastD[module[key]][i] = module[i] + + ds = module["dashboard_data"] + if "time_utc" in ds and ds["time_utc"] > limit: + lastD[module[key]] = ds.copy() + lastD[module[key]]["When"] = lastD[module[key]].pop("time_utc") + + # For potential use, add battery and radio coverage information to module data if present + for i in ( + "rf_status", + "battery_vp", + "battery_percent", + "reachable", + "wifi_status", + ): + if i in module: + lastD[module[key]][i] = module[i] + return lastD - def checkNotUpdated(self, station=None, delay=3600): + def check_not_updated(self, station_id: str, delay: int = 3600): """Check if a given station has not been updated.""" - res = self.lastData(station) + res = self.get_last_data(station_id) ret = [] for mn, v in res.items(): if time.time() - v["When"] > delay: ret.append(mn) - return ret if ret else None + return ret - def checkUpdated(self, station=None, delay=3600): + def check_updated(self, station_id: str, delay: int = 3600): """Check if a given station has been updated.""" - res = self.lastData(station) + res = self.get_last_data(station_id) ret = [] for mn, v in res.items(): if time.time() - v["When"] < delay: ret.append(mn) - return ret if ret else None + return ret - def getMeasure( + def get_measure( self, device_id, scale, @@ -254,59 +225,59 @@ def getMeasure( optimize=False, real_time=False, ): + """Retrieve data from a device or module.""" postParams = {"device_id": device_id} if module_id: postParams["module_id"] = module_id + postParams["scale"] = scale postParams["type"] = mtype + if date_begin: postParams["date_begin"] = date_begin + if date_end: postParams["date_end"] = date_end + if limit: postParams["limit"] = limit + postParams["optimize"] = "true" if optimize else "false" postParams["real_time"] = "true" if real_time else "false" - return self.authData.post_request(url=_GETMEASURE_REQ, params=postParams) - - def MinMaxTH(self, station=None, module=None, frame="last24"): - if not station: - station = self.default_station - s = self.stationByName(station) - if not s: - s = self.stationById(station) - if not s: - return None + + return self.auth_data.post_request(url=_GETMEASURE_REQ, params=postParams) + + def get_min_max_t_h( + self, station_id: str, module_id: str = None, frame: str = "last24" + ): + """Return minimum and maximum temperature and humidity over the given timeframe. + + Arguments: + station_id {str} -- Station ID + + Keyword Arguments: + module_id {str} -- Module ID (default: {None}) + frame {str} -- Timeframe can be "last24" or "day" (default: {"last24"}) + + Returns: + (min_t {float}, max_t {float}, min_h {float}, max_h {float}) -- minimum and maximum for temperature and humidity + """ if frame == "last24": end = time.time() start = end - 24 * 3600 # 24 hours ago elif frame == "day": start, end = todayStamps() - if module and module != s["module_name"]: - m = self.moduleByName(module, s["station_name"]) - if not m: - m = self.moduleById(s["_id"], module) - if not m: - return None - # retrieve module's data - resp = self.getMeasure( - device_id=s["_id"], - module_id=m["_id"], - scale="max", - mtype="Temperature,Humidity", - date_begin=start, - date_end=end, - ) - else: # retrieve station's data - resp = self.getMeasure( - device_id=s["_id"], - scale="max", - mtype="Temperature,Humidity", - date_begin=start, - date_end=end, - ) + + resp = self.get_measure( + device_id=station_id, + module_id=module_id, + scale="max", + mtype="Temperature,Humidity", + date_begin=start, + date_end=end, + ) + if resp: T = [v[0] for v in resp["body"].values()] H = [v[1] for v in resp["body"].values()] return min(T), max(T), min(H), max(H) - return None diff --git a/tests/test_pyatmo_homecoach.py b/tests/test_pyatmo_homecoach.py index eb360385c..014b39b8e 100644 --- a/tests/test_pyatmo_homecoach.py +++ b/tests/test_pyatmo_homecoach.py @@ -11,10 +11,9 @@ def test_HomeCoachData(homeCoachData): @pytest.mark.parametrize( - "station, expected", + "station_id, expected", [ - (None, ["Bedroom", "Indoor", "Kitchen", "Livingroom"]), - ("Bedroom", ["Bedroom"]), + ("12:34:56:26:69:0c", ["Bedroom"]), pytest.param( "NoValidStation", None, @@ -24,45 +23,16 @@ def test_HomeCoachData(homeCoachData): ), ], ) -def test_HomeCoachData_modulesNamesList(homeCoachData, station, expected): - assert sorted(homeCoachData.modulesNamesList(station)) == expected +def test_HomeCoachData_get_module_names(homeCoachData, station_id, expected): + assert sorted(homeCoachData.get_module_names(station_id)) == expected @pytest.mark.parametrize( - "station, expected", + "station_id, expected", [ + (None, {}), ( - None, - { - "12:34:56:26:69:0c": { - "station_name": "Bedroom", - "module_name": "Bedroom", - "id": "12:34:56:26:69:0c", - }, - "12:34:56:25:cf:a8": { - "station_name": "Kitchen", - "module_name": "Kitchen", - "id": "12:34:56:25:cf:a8", - }, - "12:34:56:26:65:14": { - "station_name": "Livingroom", - "module_name": "Livingroom", - "id": "12:34:56:26:65:14", - }, - "12:34:56:3e:c5:46": { - "station_name": "Parents Bedroom", - "module_name": "Indoor", - "id": "12:34:56:3e:c5:46", - }, - "12:34:56:26:68:92": { - "station_name": "Baby Bedroom", - "module_name": "Indoor", - "id": "12:34:56:26:68:92", - }, - }, - ), - ( - "Bedroom", + "12:34:56:26:69:0c", { "12:34:56:26:69:0c": { "station_name": "Bedroom", @@ -80,8 +50,8 @@ def test_HomeCoachData_modulesNamesList(homeCoachData, station, expected): ), ], ) -def test_HomeCoachData_getModules(homeCoachData, station, expected): - assert homeCoachData.getModules(station) == expected +def test_HomeCoachData_get_modules(homeCoachData, station_id, expected): + assert homeCoachData.get_modules(station_id) == expected def test_HomeCoachData_no_devices(auth, requests_mock): diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 87ea4e178..1118943c5 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -42,29 +42,10 @@ def test_WeatherStationData_no_data(auth, requests_mock): @pytest.mark.parametrize( - "station, expected", + "station_id, expected", [ ( - None, - [ - "Garden", - "Indoor", - "Inne - Nere", - "Inne - Uppe", - "Kitchen", - "Livingroom", - "Module", - "NAMain", - "NetatmoIndoor", - "NetatmoOutdoor", - "Rain Gauge", - "Regnmätare", - "Ute", - "Yard", - ], - ), - ( - "MyStation", + "12:34:56:37:11:ca", [ "Garden", "Kitchen", @@ -74,7 +55,7 @@ def test_WeatherStationData_no_data(auth, requests_mock): "Yard", ], ), - ("Valley Road", ["Module", "NAMain", "Rain Gauge"],), + ("12:34:56:36:fd:3c", ["Module", "NAMain", "Rain Gauge"],), pytest.param( "NoValidStation", None, @@ -84,90 +65,16 @@ def test_WeatherStationData_no_data(auth, requests_mock): ), ], ) -def test_WeatherStationData_modulesNamesList(weatherStationData, station, expected): - assert sorted(weatherStationData.modulesNamesList(station)) == expected +def test_WeatherStationData_get_module_names(weatherStationData, station_id, expected): + assert sorted(weatherStationData.get_module_names(station_id)) == expected @pytest.mark.parametrize( - "station, expected", + "station_id, expected", [ + (None, {},), ( - None, - { - "12:34:56:02:b3:da": { - "id": "12:34:56:02:b3:da", - "module_name": "Regnmätare", - "station_name": "BolÃĨs", - }, - "12:34:56:03:1b:e4": { - "id": "12:34:56:03:1b:e4", - "module_name": "Garden", - "station_name": "MyStation", - }, - "12:34:56:03:76:60": { - "id": "12:34:56:03:76:60", - "module_name": "Inne - Uppe", - "station_name": "BolÃĨs", - }, - "12:34:56:05:51:20": { - "id": "12:34:56:05:51:20", - "module_name": "Yard", - "station_name": "MyStation", - }, - "12:34:56:07:bb:0e": { - "id": "12:34:56:07:bb:0e", - "module_name": "Livingroom", - "station_name": "MyStation", - }, - "12:34:56:07:bb:3e": { - "id": "12:34:56:07:bb:3e", - "module_name": "Kitchen", - "station_name": "MyStation", - }, - "12:34:56:36:fc:de": { - "id": "12:34:56:36:fc:de", - "module_name": "NetatmoOutdoor", - "station_name": "MyStation", - }, - "12:34:56:37:11:ca": { - "id": "12:34:56:37:11:ca", - "module_name": "NetatmoIndoor", - "station_name": "MyStation", - }, - "12:34:56:1c:68:2e": { - "id": "12:34:56:1c:68:2e", - "module_name": "Inne - Nere", - "station_name": "BolÃĨs", - }, - "12:34:56:32:a7:60": { - "id": "12:34:56:32:a7:60", - "module_name": "Indoor", - "station_name": "Ateljen", - }, - "12:34:56:32:db:06": { - "id": "12:34:56:32:db:06", - "module_name": "Ute", - "station_name": "BolÃĨs", - }, - "12:34:56:36:e6:c0": { - "id": "12:34:56:36:e6:c0", - "module_name": "Module", - "station_name": "Valley Road", - }, - "12:34:56:36:fd:3c": { - "id": "12:34:56:36:fd:3c", - "module_name": "NAMain", - "station_name": "Valley Road", - }, - "12:34:56:05:25:6e": { - "id": "12:34:56:05:25:6e", - "module_name": "Rain Gauge", - "station_name": "Valley Road", - }, - }, - ), - ( - "MyStation", + "12:34:56:37:11:ca", { "12:34:56:03:1b:e4": { "id": "12:34:56:03:1b:e4", @@ -210,12 +117,13 @@ def test_WeatherStationData_modulesNamesList(weatherStationData, station, expect ), ], ) -def test_WeatherStationData_getModules(weatherStationData, station, expected): - assert weatherStationData.getModules(station) == expected +def test_WeatherStationData_get_modules(weatherStationData, station_id, expected): + assert weatherStationData.get_modules(station_id) == expected + +def test_WeatherStationData_get_station(weatherStationData): + result = weatherStationData.get_station("12:34:56:37:11:ca") -def test_WeatherStationData_stationByName(weatherStationData): - result = weatherStationData.stationByName() assert result["_id"] == "12:34:56:37:11:ca" assert result["station_name"] == "MyStation" assert result["module_name"] == "NetatmoIndoor" @@ -227,28 +135,8 @@ def test_WeatherStationData_stationByName(weatherStationData): "Noise", "Pressure", ] - assert weatherStationData.stationByName("NoValidStation") is None - -@pytest.mark.parametrize( - "module, station, expected", - [ - ("Kitchen", None, "12:34:56:07:bb:3e"), - ("Kitchen", "MyStation", "12:34:56:07:bb:3e"), - ("Kitchen", "NoValidStation", None), - ("NetatmoIndoor", None, "12:34:56:37:11:ca"), - ("NetatmoIndoor", "MyStation", "12:34:56:37:11:ca"), - ("", None, None), - ("", "", None), - (None, None, None), - ], -) -def test_WeatherStationData_moduleByName(weatherStationData, module, station, expected): - mod = weatherStationData.moduleByName(module, station) - if mod: - assert mod["_id"] == expected - else: - assert mod is expected + assert weatherStationData.get_station("NoValidStation") == {} @pytest.mark.parametrize( @@ -256,24 +144,22 @@ def test_WeatherStationData_moduleByName(weatherStationData, module, station, ex [ ("12:34:56:07:bb:3e", None, "12:34:56:07:bb:3e"), ("12:34:56:07:bb:3e", "12:34:56:37:11:ca", "12:34:56:07:bb:3e"), - ("", None, None), - ("", "", None), - (None, None, None), + ("", None, {}), + ("", "", {}), + (None, None, {}), ], ) -def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): - mod = weatherStationData.moduleById(mid, sid) - if mod: - assert mod["_id"] == expected - else: - assert mod is expected +def test_WeatherStationData_get_module(weatherStationData, mid, sid, expected): + mod = weatherStationData.get_module(mid, sid) + + assert type(mod) == dict + assert mod.get("_id", mod) == expected @pytest.mark.parametrize( - "module, moduleId, expected", + "module_id, expected", [ ( - None, "12:34:56:07:bb:3e", [ "CO2", @@ -288,8 +174,7 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): ], ), ( - "Kitchen", - None, + "12:34:56:07:bb:3e", [ "CO2", "Humidity", @@ -303,8 +188,7 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): ], ), ( - "Garden", - None, + "12:34:56:03:1b:e4", [ "GustAngle", "GustStrength", @@ -317,8 +201,7 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): ], ), ( - "Yard", - None, + "12:34:56:05:51:20", [ "Rain", "battery_percent", @@ -330,8 +213,7 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): ], ), ( - "NetatmoIndoor", - None, + "12:34:56:37:11:ca", [ "CO2", "Humidity", @@ -345,137 +227,69 @@ def test_WeatherStationData_moduleById(weatherStationData, mid, sid, expected): ], ), pytest.param( - "12:34:56:07:bb:3e", - None, - None, - marks=pytest.mark.xfail(reason="Invalid module names are not handled yet."), - ), - pytest.param( - "", None, None, marks=pytest.mark.xfail(reason="Invalid module names are not handled yet."), ), - pytest.param( - None, - None, - None, - marks=pytest.mark.xfail(reason="Invalid module names are not handled yet."), - ), - ], -) -def test_WeatherStationData_monitoredConditions( - weatherStationData, module, moduleId, expected -): - assert ( - sorted(weatherStationData.monitoredConditions(module=module, moduleId=moduleId)) - == expected - ) - - -@freeze_time("2019-06-11") -@pytest.mark.parametrize( - "station, exclude, byId, expected", - [ - ( - "MyStation", - None, - False, - [ - "Garden", - "Kitchen", - "Livingroom", - "NetatmoIndoor", - "NetatmoOutdoor", - "Yard", - ], - ), - ( - "", - None, - False, - [ - "Garden", - "Kitchen", - "Livingroom", - "NetatmoIndoor", - "NetatmoOutdoor", - "Yard", - ], - ), - ("NoValidStation", None, False, {}), - ( - None, - 1000000, - False, - [ - "Garden", - "Indoor", - "Inne - Nere", - "Inne - Uppe", - "Kitchen", - "Livingroom", - "NetatmoIndoor", - "NetatmoOutdoor", - "Rain Gauge", - "Regnmätare", - "Ute", - "Yard", - ], - ), - ( - None, - 798103, - False, - [ - "Garden", - "Indoor", - "Inne - Nere", - "Inne - Uppe", - "Kitchen", - "NetatmoIndoor", - "NetatmoOutdoor", - "Rain Gauge", - "Regnmätare", - "Ute", - "Yard", - ], - ), - ( - None, - 798103, - True, - [ - "12:34:56:02:b3:da", - "12:34:56:03:1b:e4", - "12:34:56:03:76:60", - "12:34:56:05:25:6e", - "12:34:56:05:51:20", - "12:34:56:07:bb:3e", - "12:34:56:1c:68:2e", - "12:34:56:32:a7:60", - "12:34:56:32:db:06", - "12:34:56:36:fc:de", - "12:34:56:36:fd:3c", - "12:34:56:37:11:ca", - ], - ), ], ) -def test_WeatherStationData_lastData( - weatherStationData, station, exclude, byId, expected +def test_WeatherStationData_get_monitored_conditions( + weatherStationData, module_id, expected ): - mod = weatherStationData.lastData(station=station, exclude=exclude, byId=byId) - if mod: - assert sorted(mod) == expected - else: - assert mod == expected + assert sorted(weatherStationData.get_monitored_conditions(module_id)) == expected + + +# @freeze_time("2019-06-11") +# @pytest.mark.parametrize( +# "station_id, exclude, expected", +# [ +# ( +# "12:34:56:05:51:20", +# None, +# [ +# "Garden", +# "Kitchen", +# "Livingroom", +# "NetatmoIndoor", +# "NetatmoOutdoor", +# "Yard", +# ], +# ), +# ( +# "12:34:56:37:11:ca", +# 798103, +# [ +# "12:34:56:02:b3:da", +# "12:34:56:03:1b:e4", +# "12:34:56:03:76:60", +# "12:34:56:05:25:6e", +# "12:34:56:05:51:20", +# "12:34:56:07:bb:3e", +# "12:34:56:1c:68:2e", +# "12:34:56:32:a7:60", +# "12:34:56:32:db:06", +# "12:34:56:36:fc:de", +# "12:34:56:36:fd:3c", +# "12:34:56:37:11:ca", +# ], +# ), +# ], +# ) +# def test_WeatherStationData_get_last_data( +# weatherStationData, station_id, exclude, expected +# ): +# mod = weatherStationData.get_last_data(station_id, exclude=exclude) +# if mod: +# assert sorted(mod) == expected +# else: +# assert mod == expected @freeze_time("2019-06-11") @pytest.mark.parametrize( - "station, exclude, expected", + "station_id, exclude, expected", [ + ("12:34:56:05:51:20", None, {},), ( "12:34:56:37:11:ca", None, @@ -515,10 +329,10 @@ def test_WeatherStationData_lastData( ), ], ) -def test_WeatherStationData_lastData_byId( - weatherStationData, station, exclude, expected +def test_WeatherStationData_get_last_data( + weatherStationData, station_id, exclude, expected ): - mod = weatherStationData.lastData(station, exclude, byId=True) + mod = weatherStationData.get_last_data(station_id, exclude=exclude) if mod: assert sorted(mod) == expected else: @@ -527,44 +341,21 @@ def test_WeatherStationData_lastData_byId( @freeze_time("2019-06-11") @pytest.mark.parametrize( - "station, delay, expected", + "station_id, delay, expected", [ ( - "MyStation", - 3600, - [ - "Garden", - "Kitchen", - "Livingroom", - "NetatmoIndoor", - "NetatmoOutdoor", - "Yard", - ], - ), - ( - None, - 3600, - [ - "Garden", - "Kitchen", - "Livingroom", - "NetatmoIndoor", - "NetatmoOutdoor", - "Yard", - ], - ), - ( - "", + "12:34:56:37:11:ca", 3600, [ - "Garden", - "Kitchen", - "Livingroom", - "NetatmoIndoor", - "NetatmoOutdoor", - "Yard", + "12:34:56:03:1b:e4", + "12:34:56:05:51:20", + "12:34:56:07:bb:0e", + "12:34:56:07:bb:3e", + "12:34:56:36:fc:de", + "12:34:56:37:11:ca", ], ), + ("12:34:56:37:11:ca", 798500, [],), pytest.param( "NoValidStation", 3600, @@ -573,59 +364,47 @@ def test_WeatherStationData_lastData_byId( ), ], ) -def test_WeatherStationData_checkNotUpdated( - weatherStationData, station, delay, expected +def test_WeatherStationData_check_not_updated( + weatherStationData, station_id, delay, expected ): - mod = weatherStationData.checkNotUpdated(station, delay) + mod = weatherStationData.check_not_updated(station_id, delay) assert sorted(mod) == expected @freeze_time("2019-06-11") @pytest.mark.parametrize( - "station, delay, expected", + "station_id, delay, expected", [ ( - "MyStation", - 798500, - [ - "Garden", - "Kitchen", - "Livingroom", - "NetatmoIndoor", - "NetatmoOutdoor", - "Yard", - ], - ), - ( - None, + "12:34:56:37:11:ca", 798500, [ - "Garden", - "Indoor", - "Inne - Nere", - "Inne - Uppe", - "Kitchen", - "Livingroom", - "NetatmoIndoor", - "NetatmoOutdoor", - "Rain Gauge", - "Regnmätare", - "Ute", - "Yard", + "12:34:56:03:1b:e4", + "12:34:56:05:51:20", + "12:34:56:07:bb:0e", + "12:34:56:07:bb:3e", + "12:34:56:36:fc:de", + "12:34:56:37:11:ca", ], ), + ("12:34:56:37:11:ca", 100, [],), ], ) -def test_WeatherStationData_checkUpdated(weatherStationData, station, delay, expected): - mod = weatherStationData.checkUpdated(station, delay) - assert sorted(mod) == expected +def test_WeatherStationData_check_updated( + weatherStationData, station_id, delay, expected +): + mod = weatherStationData.check_updated(station_id, delay) + if mod: + assert sorted(mod) == expected + else: + assert mod == expected @freeze_time("2019-06-11") @pytest.mark.parametrize( "device_id, scale, mtype, expected", [("MyStation", "scale", "type", [28.1])] ) -def test_WeatherStationData_getMeasure( +def test_WeatherStationData_get_measure( weatherStationData, requests_mock, device_id, scale, mtype, expected ): with open("fixtures/weatherstation_measure.json") as f: @@ -636,26 +415,30 @@ def test_WeatherStationData_getMeasure( headers={"content-type": "application/json"}, ) assert ( - weatherStationData.getMeasure(device_id, scale, mtype)["body"]["1544558433"] + weatherStationData.get_measure(device_id, scale, mtype)["body"]["1544558433"] == expected ) -def test_WeatherStationData_lastData_measurements(weatherStationData): - mod = weatherStationData.lastData("MyStation", None) - assert mod["NetatmoIndoor"]["min_temp"] == 23.4 - assert mod["NetatmoIndoor"]["max_temp"] == 25.6 - assert mod["NetatmoIndoor"]["Temperature"] == 24.6 - assert mod["NetatmoIndoor"]["Pressure"] == 1017.3 - assert mod["Garden"]["WindAngle"] == 217 - assert mod["Garden"]["WindStrength"] == 4 - assert mod["Garden"]["GustAngle"] == 206 - assert mod["Garden"]["GustStrength"] == 9 +def test_WeatherStationData_get_last_data_measurements(weatherStationData): + station_id = "12:34:56:37:11:ca" + module_id = "12:34:56:03:1b:e4" + + mod = weatherStationData.get_last_data(station_id, None) + + assert mod[station_id]["min_temp"] == 23.4 + assert mod[station_id]["max_temp"] == 25.6 + assert mod[station_id]["Temperature"] == 24.6 + assert mod[station_id]["Pressure"] == 1017.3 + assert mod[module_id]["WindAngle"] == 217 + assert mod[module_id]["WindStrength"] == 4 + assert mod[module_id]["GustAngle"] == 206 + assert mod[module_id]["GustStrength"] == 9 @freeze_time("2019-06-11") @pytest.mark.parametrize( - "station, exclude, expected", + "station_id, exclude, expected", [ ( "12:34:56:37:11:ca", @@ -669,32 +452,14 @@ def test_WeatherStationData_lastData_measurements(weatherStationData): "12:34:56:37:11:ca", ], ), - ( - None, - None, - [ - "12:34:56:02:b3:da", - "12:34:56:03:1b:e4", - "12:34:56:03:76:60", - "12:34:56:05:25:6e", - "12:34:56:05:51:20", - "12:34:56:07:bb:0e", - "12:34:56:07:bb:3e", - "12:34:56:1c:68:2e", - "12:34:56:32:a7:60", - "12:34:56:32:db:06", - "12:34:56:36:fc:de", - "12:34:56:36:fd:3c", - "12:34:56:37:11:ca", - ], - ), + (None, None, {},), ("12:34:56:00:aa:01", None, {},), ], ) -def test_WeatherStationData_lastData_bug_97( - weatherStationData, station, exclude, expected +def test_WeatherStationData_get_last_data_bug_97( + weatherStationData, station_id, exclude, expected ): - mod = weatherStationData.lastData(station, exclude, byId=True) + mod = weatherStationData.get_last_data(station_id, exclude) if mod: assert sorted(mod) == expected else: From f532119c7075fa7c63b79368de61bb1d9d87efe3 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 29 May 2020 22:37:09 +0200 Subject: [PATCH 238/333] Omit __main__ from coverage --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..b3c58f623 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = */__main__.py From 8913207e417082283e6848868ba44d214435fcf4 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 29 May 2020 23:25:31 +0200 Subject: [PATCH 239/333] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e8e92c750..9ad5c633a 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="pyatmo", - version="3.3.1", # Should be updated with new versions + version="4.0.0", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", packages=find_packages(exclude=["tests"], where="src"), From 3c533c8e0a909c2cd0643ca486ca8b028100ba46 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 30 May 2020 01:16:16 +0200 Subject: [PATCH 240/333] Remove old/deprecated methods --- src/pyatmo/public_data.py | 35 ++++++++------------------------- tests/conftest.py | 8 +++++++- tests/test_pyatmo_publicdata.py | 20 +++++++++++-------- 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/src/pyatmo/public_data.py b/src/pyatmo/public_data.py index 18e655b82..addb462fd 100644 --- a/src/pyatmo/public_data.py +++ b/src/pyatmo/public_data.py @@ -2,10 +2,6 @@ from .helpers import _BASE_URL, toTimeString _GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata" -_LON_NE = 6.221652 -_LAT_NE = 46.610870 -_LON_SW = 6.217828 -_LAT_SW = 46.596485 _STATION_TEMPERATURE_TYPE = "temperature" _STATION_PRESSURE_TYPE = "pressure" @@ -26,13 +22,13 @@ class PublicData: def __init__( self, authData, - LAT_NE=_LAT_NE, - LON_NE=_LON_NE, - LAT_SW=_LAT_SW, - LON_SW=_LON_SW, - required_data_type=None, # comma-separated list from above _STATION or _ACCESSORY values - filtering=False, - ): + LAT_NE: str, + LON_NE: str, + LAT_SW: str, + LON_SW: str, + required_data_type: str = None, # comma-separated list from above _STATION or _ACCESSORY values + filtering: bool = False, + ) -> None: self.authData = authData postParams = { "lat_ne": LAT_NE, @@ -50,6 +46,7 @@ def __init__( self.raw_data = resp["body"] except (KeyError, TypeError): raise NoDevice("No public weather data returned by Netatmo server") + self.status = resp["status"] self.time_exec = toTimeString(resp["time_exec"]) self.time_server = toTimeString(resp["time_server"]) @@ -57,30 +54,18 @@ def __init__( def CountStationInArea(self): return len(self.raw_data) - # Backwards compatibility for < 1.2 - def getLive(self): - return self.getLatestRain() - def getLatestRain(self): return self.getAccessoryMeasures(_ACCESSORY_RAIN_LIVE_TYPE) def getAverageRain(self): return averageMeasure(self.getLatestRain()) - # Backwards compatibility for < 1.2 - def get60min(self): - return self.get60minRain() - def get60minRain(self): return self.getAccessoryMeasures(_ACCESSORY_RAIN_60MIN_TYPE) def getAverage60minRain(self): return averageMeasure(self.get60minRain()) - # Backwards compatibility for < 1.2 - def get24h(self): - return self.get24hRain() - def get24hRain(self): return self.getAccessoryMeasures(_ACCESSORY_RAIN_24H_TYPE) @@ -129,10 +114,6 @@ def getLocations(self): locations[station["_id"]] = station["place"]["location"] return locations - # Backwards compatibility for < 1.2 - def getTimeforMeasure(self): - return self.getTimeForRainMeasures() - def getTimeForRainMeasures(self): return self.getAccessoryMeasures(_ACCESSORY_RAIN_TIME_TYPE) diff --git a/tests/conftest.py b/tests/conftest.py index b23a37ef2..474b2ae22 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -64,7 +64,13 @@ def publicData(auth, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - return pyatmo.PublicData(auth) + + lon_ne = 6.221652 + lat_ne = 46.610870 + lon_sw = 6.217828 + lat_sw = 46.596485 + + return pyatmo.PublicData(auth, lat_ne, lon_ne, lat_sw, lon_sw) @pytest.fixture(scope="function") diff --git a/tests/test_pyatmo_publicdata.py b/tests/test_pyatmo_publicdata.py index d16b88c11..a51aa8fad 100644 --- a/tests/test_pyatmo_publicdata.py +++ b/tests/test_pyatmo_publicdata.py @@ -5,6 +5,11 @@ import pyatmo +LON_NE = 6.221652 +LAT_NE = 46.610870 +LON_SW = 6.217828 +LAT_SW = 46.596485 + def test_PublicData(auth, requests_mock): with open("fixtures/public_data_simple.json") as f: @@ -14,17 +19,20 @@ def test_PublicData(auth, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - publicData = pyatmo.PublicData(auth) + + publicData = pyatmo.PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW) assert publicData.status == "ok" - publicData = pyatmo.PublicData(auth, required_data_type="temperature,rain_live") + publicData = pyatmo.PublicData( + auth, LAT_NE, LON_NE, LAT_SW, LON_SW, required_data_type="temperature,rain_live" + ) assert publicData.status == "ok" def test_PublicData_unavailable(auth, requests_mock): requests_mock.post(pyatmo.public_data._GETPUBLIC_DATA, status_code=404) with pytest.raises(pyatmo.ApiError): - pyatmo.PublicData(auth) + pyatmo.PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW) def test_PublicData_error(auth, requests_mock): @@ -36,7 +44,7 @@ def test_PublicData_error(auth, requests_mock): headers={"content-type": "application/json"}, ) with pytest.raises(pyatmo.NoDevice): - pyatmo.PublicData(auth) + pyatmo.PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW) def test_PublicData_CountStationInArea(publicData): @@ -51,7 +59,6 @@ def test_PublicData_getLatestRain(publicData): "70:ee:50:36:a9:fc": 0, } assert publicData.getLatestRain() == expected - assert publicData.getLive() == expected def test_PublicData_getAverageRain(publicData): @@ -65,7 +72,6 @@ def test_PublicData_get60minRain(publicData): "70:ee:50:36:94:7c": 0.2, "70:ee:50:36:a9:fc": 0, } - assert publicData.get60min() == expected assert publicData.get60minRain() == expected @@ -80,7 +86,6 @@ def test_PublicData_get24hRain(publicData): "70:ee:50:36:94:7c": 12.322000000000001, "70:ee:50:36:a9:fc": 11.009, } - assert publicData.get24h() == expected assert publicData.get24hRain() == expected @@ -191,7 +196,6 @@ def test_PublicData_getTimeforMeasure(publicData): "70:ee:50:27:25:b0": 1560247896, "70:ee:50:36:94:7c": 1560248022, } - assert publicData.getTimeforMeasure() == expected assert publicData.getTimeForRainMeasures() == expected From 4f1ef38979363aa69726437ca3c3fdb6726054e8 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 30 May 2020 01:25:19 +0200 Subject: [PATCH 241/333] Fix issue with image retrieval --- src/pyatmo/auth.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index dc44fe406..a5248dbdb 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -110,7 +110,7 @@ def post_request( else: json_params = None - if "http://" in url: + if "https://" not in url: try: resp = requests.post(url, data=params, timeout=timeout) except requests.exceptions.ChunkedEncodingError: @@ -143,7 +143,7 @@ def query(url, params, timeout, retries): # Sleep for 1 sec to prevent authentication related # timeouts after a token refresh. sleep(1) - return query(url, params, timeout, retries - 1) + return query(url, params, timeout * 2, retries - 1) resp = query(url, params, timeout, 3) @@ -168,11 +168,10 @@ def query(url, params, timeout, retries): ) try: - return ( - resp.json() - if "application/json" in resp.headers.get("content-type") - else resp.content - ) + if "application/json" in resp.headers.get("content-type", []): + return resp.json() + if resp.content not in [b"", b"None"]: + return resp.content except (TypeError, AttributeError): LOG.debug("Invalid response %s", resp) return None From 60365112fba7c32a2e4570a6c327f1943ad2a8d2 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 2 Jun 2020 10:47:10 +0200 Subject: [PATCH 242/333] Refactor public weather method names --- src/pyatmo/public_data.py | 117 ++++++++++++++++---------------- tests/test_pyatmo_publicdata.py | 100 +++++++++++++-------------- 2 files changed, 110 insertions(+), 107 deletions(-) diff --git a/src/pyatmo/public_data.py b/src/pyatmo/public_data.py index addb462fd..a236eaddd 100644 --- a/src/pyatmo/public_data.py +++ b/src/pyatmo/public_data.py @@ -1,5 +1,8 @@ +from typing import Any, Dict + +from .auth import NetatmOAuth2 from .exceptions import NoDevice -from .helpers import _BASE_URL, toTimeString +from .helpers import _BASE_URL, to_time_string _GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata" @@ -21,7 +24,7 @@ class PublicData: def __init__( self, - authData, + authData: NetatmOAuth2, LAT_NE: str, LON_NE: str, LAT_SW: str, @@ -30,7 +33,7 @@ def __init__( filtering: bool = False, ) -> None: self.authData = authData - postParams = { + postParams: Dict = { "lat_ne": LAT_NE, "lon_ne": LON_NE, "lat_sw": LAT_SW, @@ -48,80 +51,80 @@ def __init__( raise NoDevice("No public weather data returned by Netatmo server") self.status = resp["status"] - self.time_exec = toTimeString(resp["time_exec"]) - self.time_server = toTimeString(resp["time_server"]) + self.time_exec = to_time_string(resp["time_exec"]) + self.time_server = to_time_string(resp["time_server"]) - def CountStationInArea(self): + def stations_in_area(self) -> int: return len(self.raw_data) - def getLatestRain(self): - return self.getAccessoryMeasures(_ACCESSORY_RAIN_LIVE_TYPE) + def get_latest_rain(self) -> Dict: + return self.get_accessory_data(_ACCESSORY_RAIN_LIVE_TYPE) - def getAverageRain(self): - return averageMeasure(self.getLatestRain()) + def get_average_rain(self) -> float: + return average(self.get_latest_rain()) - def get60minRain(self): - return self.getAccessoryMeasures(_ACCESSORY_RAIN_60MIN_TYPE) + def get_60_min_rain(self) -> Dict: + return self.get_accessory_data(_ACCESSORY_RAIN_60MIN_TYPE) - def getAverage60minRain(self): - return averageMeasure(self.get60minRain()) + def get_average_60_min_rain(self) -> float: + return average(self.get_60_min_rain()) - def get24hRain(self): - return self.getAccessoryMeasures(_ACCESSORY_RAIN_24H_TYPE) + def get_24_h_rain(self) -> Dict: + return self.get_accessory_data(_ACCESSORY_RAIN_24H_TYPE) - def getAverage24hRain(self): - return averageMeasure(self.get24hRain()) + def get_average_24_h_rain(self) -> float: + return average(self.get_24_h_rain()) - def getLatestPressures(self): - return self.getLatestStationMeasures(_STATION_PRESSURE_TYPE) + def get_latest_pressures(self) -> Dict: + return self.get_latest_station_measures(_STATION_PRESSURE_TYPE) - def getAveragePressure(self): - return averageMeasure(self.getLatestPressures()) + def get_average_pressure(self) -> float: + return average(self.get_latest_pressures()) - def getLatestTemperatures(self): - return self.getLatestStationMeasures(_STATION_TEMPERATURE_TYPE) + def get_latest_temperatures(self) -> Dict: + return self.get_latest_station_measures(_STATION_TEMPERATURE_TYPE) - def getAverageTemperature(self): - return averageMeasure(self.getLatestTemperatures()) + def get_average_temperature(self) -> float: + return average(self.get_latest_temperatures()) - def getLatestHumidities(self): - return self.getLatestStationMeasures(_STATION_HUMIDITY_TYPE) + def get_latest_humidities(self) -> Dict: + return self.get_latest_station_measures(_STATION_HUMIDITY_TYPE) - def getAverageHumidity(self): - return averageMeasure(self.getLatestHumidities()) + def get_average_humidity(self) -> float: + return average(self.get_latest_humidities()) - def getLatestWindStrengths(self): - return self.getAccessoryMeasures(_ACCESSORY_WIND_STRENGTH_TYPE) + def get_latest_wind_strengths(self) -> Dict: + return self.get_accessory_data(_ACCESSORY_WIND_STRENGTH_TYPE) - def getAverageWindStrength(self): - return averageMeasure(self.getLatestWindStrengths()) + def get_average_wind_strength(self) -> float: + return average(self.get_latest_wind_strengths()) - def getLatestWindAngles(self): - return self.getAccessoryMeasures(_ACCESSORY_WIND_ANGLE_TYPE) + def get_latest_wind_angles(self) -> Dict: + return self.get_accessory_data(_ACCESSORY_WIND_ANGLE_TYPE) - def getLatestGustStrengths(self): - return self.getAccessoryMeasures(_ACCESSORY_GUST_STRENGTH_TYPE) + def get_latest_gust_strengths(self) -> Dict: + return self.get_accessory_data(_ACCESSORY_GUST_STRENGTH_TYPE) - def getAverageGustStrength(self): - return averageMeasure(self.getLatestGustStrengths()) + def get_average_gust_strength(self) -> float: + return average(self.get_latest_gust_strengths()) - def getLatestGustAngles(self): - return self.getAccessoryMeasures(_ACCESSORY_GUST_ANGLE_TYPE) + def get_latest_gust_angles(self): + return self.get_accessory_data(_ACCESSORY_GUST_ANGLE_TYPE) - def getLocations(self): - locations = {} + def get_locations(self) -> Dict: + locations: Dict = {} for station in self.raw_data: locations[station["_id"]] = station["place"]["location"] return locations - def getTimeForRainMeasures(self): - return self.getAccessoryMeasures(_ACCESSORY_RAIN_TIME_TYPE) + def get_time_for_rain_measures(self) -> Dict: + return self.get_accessory_data(_ACCESSORY_RAIN_TIME_TYPE) - def getTimeForWindMeasures(self): - return self.getAccessoryMeasures(_ACCESSORY_WIND_TIME_TYPE) + def get_time_for_wind_measures(self) -> Dict: + return self.get_accessory_data(_ACCESSORY_WIND_TIME_TYPE) - def getLatestStationMeasures(self, data_type): - measures = {} + def get_latest_station_measures(self, data_type) -> Dict: + measures: Dict = {} for station in self.raw_data: for _, module in station["measures"].items(): if ( @@ -137,16 +140,16 @@ def getLatestStationMeasures(self, data_type): ] return measures - def getAccessoryMeasures(self, data_type): - measures = {} + def get_accessory_data(self, data_type: str) -> Dict[str, Any]: + data: Dict = {} for station in self.raw_data: for _, module in station["measures"].items(): if data_type in module: - measures[station["_id"]] = module[data_type] - return measures + data[station["_id"]] = module[data_type] + return data -def averageMeasure(measures): - if measures: - return sum(measures.values()) / len(measures) +def average(data: dict) -> float: + if data: + return sum(data.values()) / len(data) return 0.0 diff --git a/tests/test_pyatmo_publicdata.py b/tests/test_pyatmo_publicdata.py index a51aa8fad..995873c87 100644 --- a/tests/test_pyatmo_publicdata.py +++ b/tests/test_pyatmo_publicdata.py @@ -47,53 +47,53 @@ def test_PublicData_error(auth, requests_mock): pyatmo.PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW) -def test_PublicData_CountStationInArea(publicData): - assert publicData.CountStationInArea() == 8 +def test_PublicData_stations_in_area(publicData): + assert publicData.stations_in_area() == 8 -def test_PublicData_getLatestRain(publicData): +def test_PublicData_get_latest_rain(publicData): expected = { "70:ee:50:1f:68:9e": 0, "70:ee:50:27:25:b0": 0, "70:ee:50:36:94:7c": 0.5, "70:ee:50:36:a9:fc": 0, } - assert publicData.getLatestRain() == expected + assert publicData.get_latest_rain() == expected -def test_PublicData_getAverageRain(publicData): - assert publicData.getAverageRain() == 0.125 +def test_PublicData_get_average_rain(publicData): + assert publicData.get_average_rain() == 0.125 -def test_PublicData_get60minRain(publicData): +def test_PublicData_get_60_min_rain(publicData): expected = { "70:ee:50:1f:68:9e": 0, "70:ee:50:27:25:b0": 0, "70:ee:50:36:94:7c": 0.2, "70:ee:50:36:a9:fc": 0, } - assert publicData.get60minRain() == expected + assert publicData.get_60_min_rain() == expected -def test_PublicData_getAverage60minRain(publicData): - assert publicData.getAverage60minRain() == 0.05 +def test_PublicData_get_average_60_min_rain(publicData): + assert publicData.get_average_60_min_rain() == 0.05 -def test_PublicData_get24hRain(publicData): +def test_PublicData_get_24_h_rain(publicData): expected = { "70:ee:50:1f:68:9e": 9.999, "70:ee:50:27:25:b0": 11.716000000000001, "70:ee:50:36:94:7c": 12.322000000000001, "70:ee:50:36:a9:fc": 11.009, } - assert publicData.get24hRain() == expected + assert publicData.get_24_h_rain() == expected -def test_PublicData_getAverage24hRain(publicData): - assert publicData.getAverage24hRain() == 11.261500000000002 +def test_PublicData_get_average_24_h_rain(publicData): + assert publicData.get_average_24_h_rain() == 11.261500000000002 -def test_PublicData_getLatestPressures(publicData): +def test_PublicData_get_latest_pressures(publicData): expected = { "70:ee:50:1f:68:9e": 1007.3, "70:ee:50:27:25:b0": 1012.8, @@ -104,14 +104,14 @@ def test_PublicData_getLatestPressures(publicData): "70:ee:50:27:9f:2c": 1010.6, "70:ee:50:3c:02:78": 1011.7, } - assert publicData.getLatestPressures() == expected + assert publicData.get_latest_pressures() == expected -def test_PublicData_getAveragePressure(publicData): - assert publicData.getAveragePressure() == 1010.3499999999999 +def test_PublicData_get_average_pressure(publicData): + assert publicData.get_average_pressure() == 1010.3499999999999 -def test_PublicData_getLatestTemperatures(publicData): +def test_PublicData_get_latest_temperatures(publicData): expected = { "70:ee:50:1f:68:9e": 21.1, "70:ee:50:27:25:b0": 23.2, @@ -122,14 +122,14 @@ def test_PublicData_getLatestTemperatures(publicData): "70:ee:50:27:9f:2c": 25.5, "70:ee:50:3c:02:78": 23.3, } - assert publicData.getLatestTemperatures() == expected + assert publicData.get_latest_temperatures() == expected -def test_PublicData_getAverageTemperature(publicData): - assert publicData.getAverageTemperature() == 22.725 +def test_PublicData_get_average_temperature(publicData): + assert publicData.get_average_temperature() == 22.725 -def test_PublicData_getLatestHumidities(publicData): +def test_PublicData_get_latest_humidities(publicData): expected = { "70:ee:50:1f:68:9e": 69, "70:ee:50:27:25:b0": 60, @@ -140,42 +140,42 @@ def test_PublicData_getLatestHumidities(publicData): "70:ee:50:27:9f:2c": 56, "70:ee:50:3c:02:78": 58, } - assert publicData.getLatestHumidities() == expected + assert publicData.get_latest_humidities() == expected -def test_PublicData_getAverageHumidity(publicData): - assert publicData.getAverageHumidity() == 63.25 +def test_PublicData_get_average_humidity(publicData): + assert publicData.get_average_humidity() == 63.25 -def test_PublicData_getLatestWindStrengths(publicData): +def test_PublicData_get_latest_wind_strengths(publicData): expected = {"70:ee:50:36:a9:fc": 15} - assert publicData.getLatestWindStrengths() == expected + assert publicData.get_latest_wind_strengths() == expected -def test_PublicData_getAverageWindStrength(publicData): - assert publicData.getAverageWindStrength() == 15 +def test_PublicData_get_average_wind_strength(publicData): + assert publicData.get_average_wind_strength() == 15 -def test_PublicData_getLatestWindAngles(publicData): +def test_PublicData_get_latest_wind_angles(publicData): expected = {"70:ee:50:36:a9:fc": 17} - assert publicData.getLatestWindAngles() == expected + assert publicData.get_latest_wind_angles() == expected -def test_PublicData_getLatestGustStrengths(publicData): +def test_PublicData_get_latest_gust_strengths(publicData): expected = {"70:ee:50:36:a9:fc": 31} - assert publicData.getLatestGustStrengths() == expected + assert publicData.get_latest_gust_strengths() == expected -def test_PublicData_getAverageGustStrength(publicData): - assert publicData.getAverageGustStrength() == 31 +def test_PublicData_get_average_gust_strength(publicData): + assert publicData.get_average_gust_strength() == 31 -def test_PublicData_getLatestGustAngles(publicData): +def test_PublicData_get_latest_gust_angles(publicData): expected = {"70:ee:50:36:a9:fc": 217} - assert publicData.getLatestGustAngles() == expected + assert publicData.get_latest_gust_angles() == expected -def test_PublicData_getLocations(publicData): +def test_PublicData_get_locations(publicData): expected = { "70:ee:50:1f:68:9e": [8.795445200000017, 50.2130169], "70:ee:50:27:25:b0": [8.7807159, 50.1946167], @@ -186,22 +186,22 @@ def test_PublicData_getLocations(publicData): "70:ee:50:27:9f:2c": [8.785342, 50.193573], "70:ee:50:3c:02:78": [8.795953681700666, 50.19530139868166], } - assert publicData.getLocations() == expected + assert publicData.get_locations() == expected -def test_PublicData_getTimeforMeasure(publicData): +def test_PublicData_get_time_for_rain_measures(publicData): expected = { "70:ee:50:36:a9:fc": 1560248184, "70:ee:50:1f:68:9e": 1560248344, "70:ee:50:27:25:b0": 1560247896, "70:ee:50:36:94:7c": 1560248022, } - assert publicData.getTimeForRainMeasures() == expected + assert publicData.get_time_for_rain_measures() == expected -def test_PublicData_getTimeForWindMeasures(publicData): +def test_PublicData_get_time_for_wind_measures(publicData): expected = {"70:ee:50:36:a9:fc": 1560248190} - assert publicData.getTimeForWindMeasures() == expected + assert publicData.get_time_for_wind_measures() == expected @pytest.mark.parametrize( @@ -248,8 +248,8 @@ def test_PublicData_getTimeForWindMeasures(publicData): ), ], ) -def test_PublicData_getLatestStationMeasures(publicData, test_input, expected): - assert publicData.getLatestStationMeasures(test_input) == expected +def test_PublicData_get_latest_station_measures(publicData, test_input, expected): + assert publicData.get_latest_station_measures(test_input) == expected @pytest.mark.parametrize( @@ -262,8 +262,8 @@ def test_PublicData_getLatestStationMeasures(publicData, test_input, expected): ("wind_timeutc", {"70:ee:50:36:a9:fc": 1560248190}), ], ) -def test_PublicData_getAccessoryMeasures(publicData, test_input, expected): - assert publicData.getAccessoryMeasures(test_input) == expected +def test_PublicData_get_accessory_data(publicData, test_input, expected): + assert publicData.get_accessory_data(test_input) == expected @pytest.mark.parametrize( @@ -298,5 +298,5 @@ def test_PublicData_getAccessoryMeasures(publicData, test_input, expected): ({}, 0), ], ) -def test_PublicData_averageMeasure(test_input, expected): - assert pyatmo.public_data.averageMeasure(test_input) == expected +def test_PublicData_average(test_input, expected): + assert pyatmo.public_data.average(test_input) == expected From 8a62d4cbeca906d878ba7c04a711f7e63ff55ebc Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 2 Jun 2020 10:59:05 +0200 Subject: [PATCH 243/333] Add some typing information --- src/pyatmo/__main__.py | 10 +++---- src/pyatmo/auth.py | 12 ++++----- src/pyatmo/camera.py | 50 +++++++++++++++-------------------- src/pyatmo/helpers.py | 17 ++++++------ src/pyatmo/home_coach.py | 6 ++--- src/pyatmo/public_data.py | 6 ++--- src/pyatmo/thermostat.py | 22 +++++++-------- src/pyatmo/weather_station.py | 16 +++++------ tests/test_pyatmo.py | 14 +++++----- tests/test_pyatmo_camera.py | 5 +--- 10 files changed, 73 insertions(+), 85 deletions(-) diff --git a/src/pyatmo/__main__.py b/src/pyatmo/__main__.py index bf24b9eb6..e01049381 100644 --- a/src/pyatmo/__main__.py +++ b/src/pyatmo/__main__.py @@ -28,7 +28,7 @@ def main(): ) sys.exit(1) - authorization = ClientAuth( + auth = ClientAuth( clientId=CLIENT_ID, clientSecret=CLIENT_SECRET, username=USERNAME, @@ -37,24 +37,24 @@ def main(): ) try: - WeatherStationData(authorization) + WeatherStationData(auth) except NoDevice: if sys.stdout.isatty(): print("pyatmo.py : warning, no weather station available for testing") try: - CameraData(authorization) + CameraData(auth) except NoDevice: if sys.stdout.isatty(): print("pyatmo.py : warning, no camera available for testing") try: - HomeData(authorization) + HomeData(auth) except NoDevice: if sys.stdout.isatty(): print("pyatmo.py : warning, no thermostat available for testing") - PublicData(authorization) + PublicData(auth) # If we reach this line, all is OK diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index a5248dbdb..caae0ce8f 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -1,11 +1,11 @@ import logging from json import JSONDecodeError from time import sleep -from typing import Callable, Dict, Optional, Tuple, Union +from typing import Any, Callable, Dict, Optional, Tuple, Union import requests -from oauthlib.oauth2 import LegacyApplicationClient, TokenExpiredError -from requests_oauthlib import OAuth2Session +from oauthlib.oauth2 import LegacyApplicationClient, TokenExpiredError # type: ignore +from requests_oauthlib import OAuth2Session # type: ignore from .exceptions import ApiError from .helpers import _BASE_URL, ERRORS @@ -66,7 +66,7 @@ def __init__( token: Optional[Dict[str, str]] = None, token_updater: Optional[Callable[[str], None]] = None, scope: Optional[str] = "read_station", - ): + ) -> None: self.client_id = client_id self.client_secret = client_secret self.redirect_uri = redirect_uri @@ -99,8 +99,8 @@ def refresh_tokens(self) -> Dict[str, Union[str, int]]: return token def post_request( - self, url: str, params: Optional[Dict[str, str]] = None, timeout: int = 5 - ): + self, url: str, params: Optional[Dict] = None, timeout: int = 5, + ) -> Any: """Wrapper for post requests.""" if not params: params = {} diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 7fbcf6e7f..9f8022c81 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -1,6 +1,6 @@ import imghdr import time -from typing import Dict, Tuple +from typing import Dict, Optional, Tuple, Union from requests.exceptions import ReadTimeout @@ -20,15 +20,15 @@ class CameraData: List of Netatmo camera informations (Homes, cameras, smoke detectors, modules, events, persons) Args: - auth_data (ClientAuth): + auth (ClientAuth): Authentication information with a valid access token """ - def __init__(self, auth_data, size=15): - self.auth_data = auth_data + def __init__(self, auth, size=15): + self.auth = auth post_params = {"size": size} - resp = self.auth_data.post_request(url=_GETHOMEDATA_REQ, params=post_params) + resp = self.auth.post_request(url=_GETHOMEDATA_REQ, params=post_params) if resp is None or "body" not in resp: raise NoDevice("No device data returned by Netatmo server") @@ -103,7 +103,7 @@ def get_camera(self, cid: str) -> Dict[str, str]: for home_id, _ in self.cameras.items(): if cid in self.cameras[home_id]: return self.cameras[home_id][cid] - return None + return {} def get_module(self, mid: str): """Get module data.""" @@ -116,7 +116,7 @@ def get_smokedetector(self, sid: str): return self.smokedetectors[home][sid] return None - def camera_urls(self, cid: str) -> Tuple[str, str]: + def camera_urls(self, cid: str) -> Tuple[Union[str, None], Union[str, None]]: """ Return the vpn_url and the local_url (if available) of a given camera in order to access its live feed. @@ -131,23 +131,22 @@ def update_camera_urls(self, cid: str) -> None: if camera_data: vpn_url = camera_data.get("vpn_url") - if camera_data.get("is_local"): + if vpn_url and camera_data.get("is_local"): - def check_url(url: str) -> str: - if url is None: - return None + def check_url(url: str) -> Optional[str]: try: - resp = self.auth_data.post_request(url=f"{url}/command/ping") + resp = self.auth.post_request(url=f"{url}/command/ping") except (ApiError, ReadTimeout): - LOG.debug("Timeout validation the camera url %s", url) + LOG.debug("Timeout validation of camera url %s", url) return None else: return resp.get("local_url") temp_local_url = check_url(vpn_url) - self.cameras[home_id][cid]["local_url"] = check_url(temp_local_url) + if temp_local_url: + self.cameras[home_id][cid]["local_url"] = check_url(temp_local_url) - def get_light_state(self, cid: str) -> str: + def get_light_state(self, cid: str) -> Optional[str]: """Return the current mode of the floodlight of a presence camera.""" return self.get_camera(cid).get("light_mode_status") @@ -163,14 +162,12 @@ def persons_at_home(self, home_id=None): return at_home def set_persons_home(self, person_ids, home_id): - """ - Mark persons as home. - """ + """Mark persons as home.""" post_params = { "home_id": home_id, "person_ids[]": person_ids, } - resp = self.auth_data.post_request(url=_SETPERSONSHOME_REQ, params=post_params) + resp = self.auth.post_request(url=_SETPERSONSHOME_REQ, params=post_params) return resp def set_persons_away(self, person_id, home_id): @@ -187,10 +184,10 @@ def set_persons_away(self, person_id, home_id): "home_id": home_id, "person_id": person_id, } - resp = self.auth_data.post_request(url=_SETPERSONSAWAY_REQ, params=post_params) + resp = self.auth.post_request(url=_SETPERSONSAWAY_REQ, params=post_params) return resp - def get_person_id(self, name: str) -> str: + def get_person_id(self, name: str) -> Optional[str]: """Retrieve the ID of a person. Arguments: @@ -210,9 +207,7 @@ def get_camera_picture(self, image_id: str, key: str): "image_id": image_id, "key": key, } - resp = self.auth_data.post_request( - url=_GETCAMERAPICTURE_REQ, params=post_params - ) + resp = self.auth.post_request(url=_GETCAMERAPICTURE_REQ, params=post_params) image_type = imghdr.what("NONE.FILE", resp) return resp, image_type @@ -276,11 +271,8 @@ def update_events( "event_id": event_id, } - event_list = [] try: - resp = self.auth_data.post_request( - url=_GETEVENTSUNTIL_REQ, params=post_params - ) + resp = self.auth.post_request(url=_GETEVENTSUNTIL_REQ, params=post_params) event_list = resp["body"]["events_list"] except ApiError: pass @@ -524,7 +516,7 @@ def set_state( } try: - resp = self.auth_data.post_request(url=_SETSTATE_REQ, params=post_params) + resp = self.auth.post_request(url=_SETSTATE_REQ, params=post_params) except ApiError as err_msg: LOG.error("%s", err_msg) return False diff --git a/src/pyatmo/helpers.py b/src/pyatmo/helpers.py index 39218a950..c7bac0663 100644 --- a/src/pyatmo/helpers.py +++ b/src/pyatmo/helpers.py @@ -2,12 +2,13 @@ import time from calendar import timegm from datetime import datetime +from typing import Dict, Tuple -LOG = logging.getLogger(__name__) +LOG: logging.Logger = logging.getLogger(__name__) -_BASE_URL = "https://api.netatmo.com/" +_BASE_URL: str = "https://api.netatmo.com/" -ERRORS = { +ERRORS: Dict[int, str] = { 400: "Bad request", 401: "Unauthorized", 403: "Forbidden", @@ -19,20 +20,20 @@ } -def toTimeString(value): +def to_time_string(value: str) -> str: return datetime.utcfromtimestamp(int(value)).isoformat(sep="_") -def toEpoch(value): +def to_epoch(value: str) -> int: return timegm(time.strptime(value + "GMT", "%Y-%m-%d_%H:%M:%S%Z")) -def todayStamps(): - today = timegm(time.strptime(time.strftime("%Y-%m-%d") + "GMT", "%Y-%m-%d%Z")) +def today_stamps() -> Tuple[int, int]: + today: int = timegm(time.strptime(time.strftime("%Y-%m-%d") + "GMT", "%Y-%m-%d%Z")) return today, today + 3600 * 24 -def fixId(rawData): +def fix_id(rawData: Dict) -> Dict: if rawData: for station in rawData: station["_id"] = station["_id"].replace(" ", "") diff --git a/src/pyatmo/home_coach.py b/src/pyatmo/home_coach.py index d66d5871e..580ac42aa 100644 --- a/src/pyatmo/home_coach.py +++ b/src/pyatmo/home_coach.py @@ -8,8 +8,8 @@ class HomeCoachData(WeatherStationData): """ List the Home Couch devices (stations and modules) Args: - auth_data (ClientAuth): Authentication information with a working access Token + auth (ClientAuth): Authentication information with a working access Token """ - def __init__(self, auth_data): - super(HomeCoachData, self).__init__(auth_data, url_req=_GETHOMECOACHDATA_REQ) + def __init__(self, auth): + super(HomeCoachData, self).__init__(auth, url_req=_GETHOMECOACHDATA_REQ) diff --git a/src/pyatmo/public_data.py b/src/pyatmo/public_data.py index a236eaddd..2c7f4d5b7 100644 --- a/src/pyatmo/public_data.py +++ b/src/pyatmo/public_data.py @@ -24,7 +24,7 @@ class PublicData: def __init__( self, - authData: NetatmOAuth2, + auth: NetatmOAuth2, LAT_NE: str, LON_NE: str, LAT_SW: str, @@ -32,7 +32,7 @@ def __init__( required_data_type: str = None, # comma-separated list from above _STATION or _ACCESSORY values filtering: bool = False, ) -> None: - self.authData = authData + self.auth = auth postParams: Dict = { "lat_ne": LAT_NE, "lon_ne": LON_NE, @@ -44,7 +44,7 @@ def __init__( if required_data_type: postParams["required_data"] = required_data_type - resp = self.authData.post_request(url=_GETPUBLIC_DATA, params=postParams) + resp = self.auth.post_request(url=_GETPUBLIC_DATA, params=postParams) try: self.raw_data = resp["body"] except (KeyError, TypeError): diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index dd6ec382a..e1e7977e0 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -18,12 +18,12 @@ class HomeData: List of energy devices (relays, thermostat modules and valves) Args: - authData (ClientAuth): Authentication information with a valid access token + auth (ClientAuth): Authentication information with a valid access token """ - def __init__(self, authData): - self.authData = authData - resp = self.authData.post_request(url=_GETHOMESDATA_REQ) + def __init__(self, auth): + self.auth = auth + resp = self.auth.post_request(url=_GETHOMESDATA_REQ) if resp is None or "body" not in resp: raise NoDevice("No thermostat data returned by Netatmo server") @@ -95,9 +95,7 @@ def switch_home_schedule(self, home_id: str, schedule_id: str): "home_id": home_id, "schedule_id": schedule_id, } - resp = self.authData.post_request( - url=_SWITCHHOMESCHEDULE_REQ, params=postParams - ) + resp = self.auth.post_request(url=_SWITCHHOMESCHEDULE_REQ, params=postParams) LOG.debug("Response: %s", resp) def get_hg_temp(self, home_id: str) -> float: @@ -116,13 +114,13 @@ def get_thermostat_type(self, home_id: str, room_id: str): class HomeStatus: - def __init__(self, authData, home_id): - self.authData = authData + def __init__(self, auth, home_id): + self.auth = auth self.home_id = home_id postParams = {"home_id": self.home_id} - resp = self.authData.post_request(url=_GETHOMESTATUS_REQ, params=postParams) + resp = self.auth.post_request(url=_GETHOMESTATUS_REQ, params=postParams) if ( "errors" in resp or "body" not in resp @@ -207,7 +205,7 @@ def set_thermmode(self, mode, end_time=None, schedule_id=None): postParams["endtime"] = end_time if schedule_id is not None and mode == "schedule": postParams["schedule_id"] = schedule_id - return self.authData.post_request(url=_SETTHERMMODE_REQ, params=postParams) + return self.auth.post_request(url=_SETTHERMMODE_REQ, params=postParams) def set_room_thermpoint(self, room_id: str, mode: str, temp=None, end_time=None): postParams = { @@ -221,4 +219,4 @@ def set_room_thermpoint(self, room_id: str, mode: str, temp=None, end_time=None) postParams["temp"] = temp if end_time is not None: postParams["endtime"] = end_time - return self.authData.post_request(url=_SETROOMTHERMPOINT_REQ, params=postParams) + return self.auth.post_request(url=_SETROOMTHERMPOINT_REQ, params=postParams) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index f8aa544f1..4f5f9f168 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -3,7 +3,7 @@ from typing import Dict, List from .exceptions import NoDevice -from .helpers import _BASE_URL, fixId, todayStamps +from .helpers import _BASE_URL, fix_id, today_stamps LOG = logging.getLogger(__name__) @@ -15,20 +15,20 @@ class WeatherStationData: """ List the Weather Station devices (stations and modules) Args: - auth_data (ClientAuth): Authentication information with a working access Token + auth (ClientAuth): Authentication information with a working access Token """ - def __init__(self, auth_data, url_req: str = None): + def __init__(self, auth, url_req: str = None): """Initialize the weather station class.""" self.url_req = url_req or _GETSTATIONDATA_REQ - self.auth_data = auth_data + self.auth = auth - resp = self.auth_data.post_request(url=self.url_req) + resp = self.auth.post_request(url=self.url_req) if resp is None or "body" not in resp: raise NoDevice("No weather station data returned by Netatmo server") try: - self.rawData = fixId(resp["body"].get("devices")) + self.rawData = fix_id(resp["body"].get("devices")) except KeyError: LOG.debug("No in response %s", resp) raise NoDevice("No weather station data returned by Netatmo server") @@ -245,7 +245,7 @@ def get_measure( postParams["optimize"] = "true" if optimize else "false" postParams["real_time"] = "true" if real_time else "false" - return self.auth_data.post_request(url=_GETMEASURE_REQ, params=postParams) + return self.auth.post_request(url=_GETMEASURE_REQ, params=postParams) def get_min_max_t_h( self, station_id: str, module_id: str = None, frame: str = "last24" @@ -266,7 +266,7 @@ def get_min_max_t_h( end = time.time() start = end - 24 * 3600 # 24 hours ago elif frame == "day": - start, end = todayStamps() + start, end = today_stamps() resp = self.get_measure( device_id=station_id, diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index 8044699a3..616b5e8f2 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -84,9 +84,9 @@ def test_postRequest_fail(auth, requests_mock, test_input, expected): pytest.param({1}, None, marks=pytest.mark.xfail), ], ) -def test_toTimeString(test_input, expected): +def test_to_time_string(test_input, expected): """Test time to string conversion.""" - assert pyatmo.helpers.toTimeString(test_input) == expected + assert pyatmo.helpers.to_time_string(test_input) == expected @pytest.mark.parametrize( @@ -98,9 +98,9 @@ def test_toTimeString(test_input, expected): ("2033-05-18_03:33:20", 2000000000), ], ) -def test_toEpoch(test_input, expected): +def test_to_epoch(test_input, expected): """Test time to epoch conversion.""" - assert pyatmo.helpers.toEpoch(test_input) == expected + assert pyatmo.helpers.to_epoch(test_input) == expected @pytest.mark.parametrize( @@ -111,11 +111,11 @@ def test_toEpoch(test_input, expected): pytest.param("2000-04-31", None, marks=pytest.mark.xfail), ], ) -def test_todayStamps(monkeypatch, test_input, expected): - """Test todayStamps function.""" +def test_today_stamps(monkeypatch, test_input, expected): + """Test today_stamps function.""" def mockreturn(format): return test_input monkeypatch.setattr(time, "strftime", mockreturn) - assert pyatmo.helpers.todayStamps() == expected + assert pyatmo.helpers.today_stamps() == expected diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index b5ec790a8..70337d331 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -48,10 +48,7 @@ def test_HomeData_no_homes(auth, requests_mock): ) def test_CameraData_get_camera(cameraHomeData, cid, expected): camera = cameraHomeData.get_camera(cid) - if camera: - assert camera["name"] == expected - else: - assert camera is expected + assert camera.get("name") == expected def test_CameraData_get_module(cameraHomeData): From 01b9d52a0f4f9b5ab7a57b221267a75c43b47315 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 2 Jun 2020 11:43:45 +0200 Subject: [PATCH 244/333] Update src/pyatmo/camera.py Co-authored-by: Hugo Dupras --- src/pyatmo/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 9f8022c81..003b70671 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -116,7 +116,7 @@ def get_smokedetector(self, sid: str): return self.smokedetectors[home][sid] return None - def camera_urls(self, cid: str) -> Tuple[Union[str, None], Union[str, None]]: + def camera_urls(self, cid: str) -> Tuple[Optional[str], Optional[str]]: """ Return the vpn_url and the local_url (if available) of a given camera in order to access its live feed. From fafb41a45a2f46b851850f58b7ba3b6a391d15de Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 2 Jun 2020 12:11:24 +0200 Subject: [PATCH 245/333] Update doc strings --- src/pyatmo/auth.py | 39 ++++++++++++++++--------------- src/pyatmo/camera.py | 43 +++++++++++++++++++++-------------- src/pyatmo/home_coach.py | 15 ++++++++---- src/pyatmo/public_data.py | 19 ++++++++++++++++ src/pyatmo/thermostat.py | 31 +++++++++++++++---------- src/pyatmo/weather_station.py | 16 +++++++++---- 6 files changed, 107 insertions(+), 56 deletions(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index caae0ce8f..a8174c3a3 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -38,24 +38,6 @@ class NetatmOAuth2: """ Handle authentication with OAuth2 - - :param client_id: Application client ID delivered by Netatmo on dev.netatmo.com - :param client_secret: Application client secret delivered by Netatmo on dev.netatmo.com - :param redirect_uri: Redirect URI where to the authorization server will redirect with an authorization code - :param token: Authorization token - :param token_updater: Callback when the token is updated - :param scope: - read_station: to retrieve weather station data (Getstationsdata, Getmeasure) - read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture) - access_camera: to access the camera, the videos and the live stream - write_camera: to set home/away status of persons (Setpersonsaway, Setpersonshome) - read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata) - write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint) - read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) - access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status - read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) - read_smokedetector: to retrieve the smoke detector status (Gethomedata) - Several value can be used at the same time, ie: 'read_station read_camera' """ def __init__( @@ -67,6 +49,27 @@ def __init__( token_updater: Optional[Callable[[str], None]] = None, scope: Optional[str] = "read_station", ) -> None: + """Initialize self. + + Keyword Arguments: + client_id {str} -- Application client ID delivered by Netatmo on dev.netatmo.com (default: {None}) + client_secret {str} -- Application client secret delivered by Netatmo on dev.netatmo.com (default: {None}) + redirect_uri {Optional[str]} -- Redirect URI where to the authorization server will redirect with an authorization code (default: {None}) + token {Optional[Dict[str, str]]} -- Authorization token (default: {None}) + token_updater {Optional[Callable[[str], None]]} -- Callback when the token is updated (default: {None}) + scope {Optional[str]} -- List of scopes (default: {"read_station"}) + read_station: to retrieve weather station data (Getstationsdata, Getmeasure) + read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture) + access_camera: to access the camera, the videos and the live stream + write_camera: to set home/away status of persons (Setpersonsaway, Setpersonshome) + read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata) + write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint) + read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) + access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status + read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) + read_smokedetector: to retrieve the smoke detector status (Gethomedata) + Several values can be used at the same time, ie: 'read_station read_camera' + """ self.client_id = client_id self.client_secret = client_secret self.redirect_uri = redirect_uri diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 9f8022c81..56f16d4e2 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -4,6 +4,7 @@ from requests.exceptions import ReadTimeout +from .auth import NetatmOAuth2 from .exceptions import ApiError, NoDevice from .helpers import _BASE_URL, LOG @@ -17,14 +18,22 @@ class CameraData: """ - List of Netatmo camera informations + Class of Netatmo camera informations (Homes, cameras, smoke detectors, modules, events, persons) - Args: - auth (ClientAuth): - Authentication information with a valid access token """ - def __init__(self, auth, size=15): + def __init__(self, auth: NetatmOAuth2, size: int = 30) -> None: + """Initialize self. + + Arguments: + auth {NetatmOAuth2} -- Authentication information with a valid access token + + Keyword Arguments: + size {int} -- Number of events to retrieve. (default: {30}) + + Raises: + NoDevice: No devices found. + """ self.auth = auth post_params = {"size": size} @@ -36,21 +45,21 @@ def __init__(self, auth, size=15): if not self.raw_data: raise NoDevice("No device data available") - self.homes = {d["id"]: d for d in self.raw_data} + self.homes: Dict = {d["id"]: d for d in self.raw_data} - self.persons = {} - self.events = {} - self.outdoor_events = {} - self.cameras = {} - self.smokedetectors = {} - self.modules = {} - self.last_event = {} - self.outdoor_last_event = {} - self.types = {} + self.persons: Dict = {} + self.events: Dict = {} + self.outdoor_events: Dict = {} + self.cameras: Dict = {} + self.smokedetectors: Dict = {} + self.modules: Dict = {} + self.last_event: Dict = {} + self.outdoor_last_event: Dict = {} + self.types: Dict = {} for item in self.raw_data: - home_id = item.get("id") - home_name = item.get("name") + home_id: str = item.get("id") + home_name: str = item.get("name") if not home_name: home_name = "Unknown" self.homes[home_id]["name"] = home_name diff --git a/src/pyatmo/home_coach.py b/src/pyatmo/home_coach.py index 580ac42aa..d6fefa8ff 100644 --- a/src/pyatmo/home_coach.py +++ b/src/pyatmo/home_coach.py @@ -1,3 +1,4 @@ +from .auth import NetatmOAuth2 from .helpers import _BASE_URL from .weather_station import WeatherStationData @@ -6,10 +7,16 @@ class HomeCoachData(WeatherStationData): """ - List the Home Couch devices (stations and modules) - Args: - auth (ClientAuth): Authentication information with a working access Token + Class of Netatmo Home Couch devices (stations and modules) """ - def __init__(self, auth): + def __init__(self, auth: NetatmOAuth2) -> None: + """Initialize self. + + Arguments: + auth {NetatmOAuth2} -- Authentication information with a valid access token + + Raises: + NoDevice: No devices found. + """ super(HomeCoachData, self).__init__(auth, url_req=_GETHOMECOACHDATA_REQ) diff --git a/src/pyatmo/public_data.py b/src/pyatmo/public_data.py index 2c7f4d5b7..5df44ba66 100644 --- a/src/pyatmo/public_data.py +++ b/src/pyatmo/public_data.py @@ -22,6 +22,10 @@ class PublicData: + """ + Class of Netatmo public weather data. + """ + def __init__( self, auth: NetatmOAuth2, @@ -32,6 +36,21 @@ def __init__( required_data_type: str = None, # comma-separated list from above _STATION or _ACCESSORY values filtering: bool = False, ) -> None: + """Initialize self. + + Arguments: + auth {NetatmOAuth2} -- Authentication information with a valid access token + LAT_NE {str} -- Latitude of the north east corner of the requested area. (-85 <= LAT_NE <= 85 and LAT_NE > LAT_SW) + LON_NE {str} -- Longitude of the north east corner of the requested area. (-180 <= LON_NE <= 180 and LON_NE > LON_SW) + LAT_SW {str} -- latitude of the south west corner of the requested area. (-85 <= LAT_SW <= 85) + LON_SW {str} -- Longitude of the south west corner of the requested area. (-180 <= LON_SW <= 180) + + Keyword Arguments: + required_data_type {str} -- [description] (default: {None}) + + Raises: + NoDevice: No devices found. + """ self.auth = auth postParams: Dict = { "lat_ne": LAT_NE, diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index e1e7977e0..59a0761ab 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -1,5 +1,7 @@ import logging +from typing import Dict +from .auth import NetatmOAuth2 from .exceptions import InvalidRoom, NoDevice, NoSchedule from .helpers import _BASE_URL @@ -15,13 +17,18 @@ class HomeData: """ - List of energy devices (relays, thermostat modules and valves) - - Args: - auth (ClientAuth): Authentication information with a valid access token + Class of Netatmo energy devices (relays, thermostat modules and valves) """ - def __init__(self, auth): + def __init__(self, auth: NetatmOAuth2) -> None: + """Initialize self. + + Arguments: + auth {NetatmOAuth2} -- Authentication information with a valid access token + + Raises: + NoDevice: No devices found. + """ self.auth = auth resp = self.auth.post_request(url=_GETHOMESDATA_REQ) if resp is None or "body" not in resp: @@ -31,13 +38,13 @@ def __init__(self, auth): if not self.rawData: raise NoDevice("No thermostat data available") - self.homes = {d["id"]: d for d in self.rawData} + self.homes: Dict = {d["id"]: d for d in self.rawData} - self.modules = {} - self.rooms = {} - self.schedules = {} - self.zones = {} - self.setpoint_duration = {} + self.modules: Dict = {} + self.rooms: Dict = {} + self.schedules: Dict = {} + self.zones: Dict = {} + self.setpoint_duration: Dict = {} for item in self.rawData: home_id = item.get("id") @@ -86,7 +93,7 @@ def switch_home_schedule(self, home_id: str, schedule_id: str): """.""" schedules = { self.schedules[home_id][s]["name"]: self.schedules[home_id][s]["id"] - for s in self.schedules.get(home_id) + for s in self.schedules.get(home_id, {}) } if schedule_id not in list(schedules.values()): raise NoSchedule("%s is not a valid schedule id" % schedule_id) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 4f5f9f168..b0fc6a7b6 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -2,6 +2,7 @@ import time from typing import Dict, List +from .auth import NetatmOAuth2 from .exceptions import NoDevice from .helpers import _BASE_URL, fix_id, today_stamps @@ -13,13 +14,18 @@ class WeatherStationData: """ - List the Weather Station devices (stations and modules) - Args: - auth (ClientAuth): Authentication information with a working access Token + Class of Netatmo Weather Station devices (stations and modules) """ - def __init__(self, auth, url_req: str = None): - """Initialize the weather station class.""" + def __init__(self, auth: NetatmOAuth2, url_req: str = None) -> None: + """Initialize self. + + Arguments: + auth {NetatmOAuth2} -- Authentication information with a valid access token + + Raises: + NoDevice: No devices found. + """ self.url_req = url_req or _GETSTATIONDATA_REQ self.auth = auth From 2ff2840b33651d74f59b5b006d4e116e8434a536 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 2 Jun 2020 12:12:32 +0200 Subject: [PATCH 246/333] Fix --- src/pyatmo/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 8da395b42..c56dcbe64 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -1,6 +1,6 @@ import imghdr import time -from typing import Dict, Optional, Tuple, Union +from typing import Dict, Optional, Tuple from requests.exceptions import ReadTimeout From 049ab5f9b9ead444422467cd4ff6332217842688 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 2 Jun 2020 16:04:38 +0200 Subject: [PATCH 247/333] Fix post merge issues --- src/pyatmo/__main__.py | 9 +- src/pyatmo/auth.py | 10 +- src/pyatmo/camera.py | 298 +++++++++++++++------------- src/pyatmo/helpers.py | 6 +- src/pyatmo/home_coach.py | 8 +- src/pyatmo/public_data.py | 10 +- src/pyatmo/thermostat.py | 88 ++++---- src/pyatmo/weather_station.py | 118 +++++------ tests/conftest.py | 14 +- tests/test_pyatmo.py | 14 +- tests/test_pyatmo_camera.py | 186 ++++++++--------- tests/test_pyatmo_homecoach.py | 8 +- tests/test_pyatmo_publicdata.py | 106 +++++----- tests/test_pyatmo_thermostat.py | 152 ++++++-------- tests/test_pyatmo_weatherstation.py | 71 ++++--- 15 files changed, 546 insertions(+), 552 deletions(-) diff --git a/src/pyatmo/__main__.py b/src/pyatmo/__main__.py index 867866dec..1ba1a12e0 100644 --- a/src/pyatmo/__main__.py +++ b/src/pyatmo/__main__.py @@ -8,6 +8,11 @@ from pyatmo.thermostat import HomeData from pyatmo.weather_station import WeatherStationData +LON_NE = 6.221652 +LAT_NE = 46.610870 +LON_SW = 6.217828 +LAT_SW = 46.596485 + def main(): try: @@ -28,7 +33,7 @@ def main(): ) sys.exit(1) - authorization = ClientAuth( + auth = ClientAuth( client_id=client_id, client_secret=client_secret, username=username, @@ -54,7 +59,7 @@ def main(): if sys.stdout.isatty(): print("pyatmo.py : warning, no thermostat available for testing") - PublicData(auth) + PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW) # If we reach this line, all is OK diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 5240574bc..c046b5053 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -8,15 +8,15 @@ from requests_oauthlib import OAuth2Session # type: ignore from pyatmo.exceptions import ApiError -from pyatmo.helpers import BASE_URL, ERRORS +from pyatmo.helpers import _BASE_URL, ERRORS LOG = logging.getLogger(__name__) # Common definitions -AUTH_REQ = BASE_URL + "oauth2/token" -AUTH_URL = BASE_URL + "oauth2/authorize" -WEBHOOK_URL_ADD = BASE_URL + "api/addwebhook" -WEBHOOK_URL_DROP = BASE_URL + "api/dropwebhook" +AUTH_REQ = _BASE_URL + "oauth2/token" +AUTH_URL = _BASE_URL + "oauth2/authorize" +WEBHOOK_URL_ADD = _BASE_URL + "api/addwebhook" +WEBHOOK_URL_DROP = _BASE_URL + "api/dropwebhook" # Possible scops diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 006dd049e..61f43386b 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -1,20 +1,19 @@ import imghdr import time -from pprint import pformat -from typing import Dict, Optional, Tuple +from typing import Dict, List, Optional, Tuple from requests.exceptions import ReadTimeout -from .auth import NetatmOAuth2 +from .auth import NetatmoOAuth2 from .exceptions import ApiError, NoDevice from .helpers import _BASE_URL, LOG -_GETHOMEDATA_REQ = BASE_URL + "api/gethomedata" -_GETCAMERAPICTURE_REQ = BASE_URL + "api/getcamerapicture" -_GETEVENTSUNTIL_REQ = BASE_URL + "api/geteventsuntil" -_SETPERSONSAWAY_REQ = BASE_URL + "api/setpersonsaway" -_SETPERSONSHOME_REQ = BASE_URL + "api/setpersonshome" -_SETSTATE_REQ = BASE_URL + "api/setstate" +_GETHOMEDATA_REQ = _BASE_URL + "api/gethomedata" +_GETCAMERAPICTURE_REQ = _BASE_URL + "api/getcamerapicture" +_GETEVENTSUNTIL_REQ = _BASE_URL + "api/geteventsuntil" +_SETPERSONSAWAY_REQ = _BASE_URL + "api/setpersonsaway" +_SETPERSONSHOME_REQ = _BASE_URL + "api/setpersonshome" +_SETSTATE_REQ = _BASE_URL + "api/setstate" class CameraData: @@ -23,11 +22,11 @@ class CameraData: (Homes, cameras, smoke detectors, modules, events, persons) """ - def __init__(self, auth: NetatmOAuth2, size: int = 30) -> None: + def __init__(self, auth: NetatmoOAuth2, size: int = 30) -> None: """Initialize self. Arguments: - auth {NetatmOAuth2} -- Authentication information with a valid access token + auth {NetatmoOAuth2} -- Authentication information with a valid access token Keyword Arguments: size {int} -- Number of events to retrieve. (default: {30}) @@ -70,31 +69,32 @@ def __init__(self, auth: NetatmOAuth2, size: int = 30) -> None: self.smokedetectors[home_id] = {} if home_id not in self.types: self.types[home_id] = {} - for p in item["persons"]: - self.persons[p["id"]] = p + for person in item["persons"]: + self.persons[person["id"]] = person if "events" in item: - for e in item["events"]: - if e["type"] == "outdoor": - if e["camera_id"] not in self.outdoor_events: - self.outdoor_events[e["camera_id"]] = {} - self.outdoor_events[e["camera_id"]][e["time"]] = e - elif e["type"] != "outdoor": - if e["camera_id"] not in self.events: - self.events[e["camera_id"]] = {} - self.events[e["camera_id"]][e["time"]] = e - for c in item["cameras"]: - self.cameras[home_id][c["id"]] = c - self.cameras[home_id][c["id"]]["home_id"] = home_id - if c["type"] == "NACamera" and "modules" in c: - for m in c["modules"]: - self.modules[m["id"]] = m - self.modules[m["id"]]["cam_id"] = c["id"] - for s in item["smokedetectors"]: - self.smokedetectors[home_id][s["id"]] = s - for t in item["cameras"]: - self.types[home_id][t["type"]] = t - for t in item["smokedetectors"]: - self.types[home_id][t["type"]] = t + for event in item["events"]: + if event["type"] == "outdoor": + if event["camera_id"] not in self.outdoor_events: + self.outdoor_events[event["camera_id"]] = {} + self.outdoor_events[event["camera_id"]][event["time"]] = event + elif event["type"] != "outdoor": + if event["camera_id"] not in self.events: + self.events[event["camera_id"]] = {} + self.events[event["camera_id"]][event["time"]] = event + for camera in item["cameras"]: + self.cameras[home_id][camera["id"]] = camera + self.cameras[home_id][camera["id"]]["home_id"] = home_id + if camera["type"] == "NACamera" and "modules" in camera: + for module in camera["modules"]: + self.modules[module["id"]] = module + self.modules[module["id"]]["cam_id"] = camera["id"] + for smoke in item["smokedetectors"]: + self.smokedetectors[home_id][smoke["id"]] = smoke + for camera_type in item["cameras"]: + self.types[home_id][camera_type["type"]] = camera_type + for smoke_type in item["smokedetectors"]: + self.types[home_id][smoke_type["type"]] = smoke_type + for camera in self.events: self.last_event[camera] = self.events[camera][ sorted(self.events[camera])[-1] @@ -108,35 +108,35 @@ def __init__(self, auth: NetatmOAuth2, size: int = 30) -> None: for camera_id in self.cameras[home_id]: self.update_camera_urls(camera_id) - def get_camera(self, cid: str) -> Optional[Dict[str, str]]: + def get_camera(self, camera_id: str) -> Dict[str, str]: """Get camera data.""" for home_id, _ in self.cameras.items(): - if cid in self.cameras[home_id]: - return self.cameras[home_id][cid] + if camera_id in self.cameras[home_id]: + return self.cameras[home_id][camera_id] return {} - def get_module(self, mid: str): + def get_module(self, module_id: str): """Get module data.""" - return None if mid not in self.modules else self.modules[mid] + return None if module_id not in self.modules else self.modules[module_id] - def get_smokedetector(self, sid: str): + def get_smokedetector(self, smoke_id: str): """Get smoke detector.""" - for home, sd in self.smokedetectors.items(): - if sid in self.smokedetectors[home]: - return self.smokedetectors[home][sid] + for home_id, _ in self.smokedetectors.items(): + if smoke_id in self.smokedetectors[home_id]: + return self.smokedetectors[home_id][smoke_id] return None - def camera_urls(self, cid: str) -> Tuple[Optional[str], Optional[str]]: + def camera_urls(self, camera_id: str) -> Tuple[Optional[str], Optional[str]]: """ Return the vpn_url and the local_url (if available) of a given camera in order to access its live feed. """ - camera_data = self.get_camera(cid) + camera_data = self.get_camera(camera_id) return camera_data.get("vpn_url", None), camera_data.get("local_url", None) - def update_camera_urls(self, cid: str) -> None: + def update_camera_urls(self, camera_id: str) -> None: """Update and validate the camera urls.""" - camera_data = self.get_camera(cid) + camera_data = self.get_camera(camera_id) home_id = camera_data["home_id"] if camera_data: @@ -154,29 +154,37 @@ def check_url(url: str) -> Optional[str]: temp_local_url = check_url(vpn_url) if temp_local_url: - self.cameras[home_id][cid]["local_url"] = check_url(temp_local_url) + self.cameras[home_id][camera_id]["local_url"] = check_url( + temp_local_url + ) - def get_light_state(self, cid: str) -> Optional[str]: + def get_light_state(self, camera_id: str) -> Optional[str]: """Return the current mode of the floodlight of a presence camera.""" - camera_data = self.get_camera(cid) + camera_data = self.get_camera(camera_id) if camera_data is None: raise ValueError("Invalid Camera ID") return camera_data.get("light_mode_status") - def persons_at_home(self, home_id=None): + def persons_at_home(self, home_id: str = None) -> List: """Return a list of known persons who are currently at home.""" - home_data = self.homes.get(home_id) + home_data = self.homes.get(home_id, {}) at_home = [] - for p in home_data["persons"]: + + for person in home_data.get("persons"): # Only check known personshome - if "pseudo" in p: - if not p["out_of_sight"]: - at_home.append(p["pseudo"]) + if "pseudo" in person: + if not person["out_of_sight"]: + at_home.append(person["pseudo"]) return at_home - def set_persons_home(self, person_ids, home_id): - """Mark persons as home.""" + def set_persons_home(self, person_ids: List[str], home_id: str): + """Mark persons as home. + + Arguments: + person_ids {list} -- IDs of persons + home_id {str} -- ID of a home + """ post_params = { "home_id": home_id, "person_ids[]": person_ids, @@ -184,15 +192,12 @@ def set_persons_home(self, person_ids, home_id): resp = self.auth.post_request(url=_SETPERSONSHOME_REQ, params=post_params) return resp - def set_persons_away(self, person_id, home_id): + def set_persons_away(self, person_id: str, home_id: str): """Mark a person as away or set the whole home to being empty. Arguments: person_id {str} -- ID of a person home_id {str} -- ID of a home - - Returns: - [type] -- [description] """ post_params = { "home_id": home_id, @@ -227,11 +232,11 @@ def get_camera_picture(self, image_id: str, key: str): def get_profile_image(self, name: str): """Retrieve the face of a given person.""" - for p in self.persons: - if "pseudo" in self.persons[p]: - if name == self.persons[p]["pseudo"]: - image_id = self.persons[p]["face"]["id"] - key = self.persons[p]["face"]["key"] + for person in self.persons: + if "pseudo" in self.persons[person]: + if name == self.persons[person]["pseudo"]: + image_id = self.persons[person]["face"]["id"] + key = self.persons[person]["face"]["key"] return self.get_camera_picture(image_id, key) return None, None @@ -274,10 +279,10 @@ def update_events( # If no event is provided we need to retrieve the oldest of # the last event by each smoke detector event_list = {} - for sid in self.outdoor_last_event: + for smoke_id in self.outdoor_last_event: event_list[ - self.outdoor_last_event[sid]["time"] - ] = self.outdoor_last_event[sid] + self.outdoor_last_event[smoke_id]["time"] + ] = self.outdoor_last_event[smoke_id] event_id = event_list[sorted(event_list)[0]]["id"] post_params = { @@ -294,15 +299,15 @@ def update_events( LOG.debug("event_list response: %s", resp) LOG.debug("event_list body: %s", resp["body"]) - for e in event_list: - if e["type"] == "outdoor": - if e["camera_id"] not in self.outdoor_events: - self.outdoor_events[e["camera_id"]] = {} - self.outdoor_events[e["camera_id"]][e["time"]] = e - elif e["type"] != "outdoor": - if e["camera_id"] not in self.events: - self.events[e["camera_id"]] = {} - self.events[e["camera_id"]][e["time"]] = e + for event in event_list: + if event["type"] == "outdoor": + if event["camera_id"] not in self.outdoor_events: + self.outdoor_events[event["camera_id"]] = {} + self.outdoor_events[event["camera_id"]][event["time"]] = event + elif event["type"] != "outdoor": + if event["camera_id"] not in self.events: + self.events[event["camera_id"]] = {} + self.events[event["camera_id"]][event["time"]] = event for camera in self.events: self.last_event[camera] = self.events[camera][ sorted(self.events[camera])[-1] @@ -312,25 +317,25 @@ def update_events( sorted(self.outdoor_events[camera])[-1] ] - def person_seen_by_camera(self, name, cid, exclude=0): + def person_seen_by_camera(self, name, camera_id, exclude=0): """ Evaluate if a specific person has been seen """ # Check in the last event is someone known has been seen if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cid], reverse=True) + array_time_event = sorted(self.events[camera_id], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False - if self.events[cid][time_ev]["type"] == "person": - person_id = self.events[cid][time_ev]["person_id"] + if self.events[camera_id][time_ev]["type"] == "person": + person_id = self.events[camera_id][time_ev]["person_id"] if "pseudo" in self.persons[person_id]: if self.persons[person_id]["pseudo"] == name: return True - - elif self.last_event[cid]["type"] == "person": - person_id = self.last_event[cid]["person_id"] + + elif self.last_event[camera_id]["type"] == "person": + person_id = self.last_event[camera_id]["person_id"] if "pseudo" in self.persons[person_id]: if self.persons[person_id]["pseudo"] == name: return True @@ -348,143 +353,154 @@ def known_persons(self): def known_persons_names(self): names = [] - for _, p in self._known_persons().items(): - names.append(p["pseudo"]) + for person in self._known_persons().values(): + names.append(person["pseudo"]) return names - def someone_known_seen(self, cid: str, exclude: int = 0) -> bool: + def someone_known_seen(self, camera_id: str, exclude: int = 0) -> bool: """Evaluate if someone known has been seen.""" - if cid not in self.events: + if camera_id not in self.events: raise NoDevice if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cid], reverse=True) + array_time_event = sorted(self.events[camera_id], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False - if self.events[cid][time_ev]["type"] == "person": - if self.events[cid][time_ev]["person_id"] in self._known_persons(): + if self.events[camera_id][time_ev]["type"] == "person": + if ( + self.events[camera_id][time_ev]["person_id"] + in self._known_persons() + ): return True # Check in the last event if someone known has been seen - elif self.last_event[cid]["type"] == "person": - if self.last_event[cid]["person_id"] in self._known_persons(): + elif self.last_event[camera_id]["type"] == "person": + if self.last_event[camera_id]["person_id"] in self._known_persons(): return True return False - def someone_unknown_seen(self, cid: str, exclude: int = 0) -> bool: + def someone_unknown_seen(self, camera_id: str, exclude: int = 0) -> bool: """Evaluate if someone known has been seen.""" - if cid not in self.events: + if camera_id not in self.events: raise NoDevice if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cid], reverse=True) + array_time_event = sorted(self.events[camera_id], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False - if self.events[cid][time_ev]["type"] == "person": + if self.events[camera_id][time_ev]["type"] == "person": if ( - self.events[cid][time_ev]["person_id"] + self.events[camera_id][time_ev]["person_id"] not in self._known_persons() ): return True # Check in the last event is someone known has been seen - elif self.last_event[cid]["type"] == "person": - if self.last_event[cid]["person_id"] not in self._known_persons(): + elif self.last_event[camera_id]["type"] == "person": + if self.last_event[camera_id]["person_id"] not in self._known_persons(): return True return False - def motion_detected(self, cid, exclude=0): + def motion_detected(self, camera_id, exclude=0): """Evaluate if movement has been detected.""" - if cid not in self.events: + if camera_id not in self.events: raise NoDevice if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events[cid], reverse=True) + array_time_event = sorted(self.events[camera_id], reverse=True) for time_ev in array_time_event: if time_ev < limit: return False - if self.events[cid][time_ev]["type"] == "movement": + if self.events[camera_id][time_ev]["type"] == "movement": return True - elif self.last_event[cid]["type"] == "movement": + elif self.last_event[camera_id]["type"] == "movement": return True return False - def outdoor_motion_detected(self, cid: str, offset: int = 0) -> bool: + def outdoor_motion_detected(self, camera_id: str, offset: int = 0) -> bool: """Evaluate if outdoor movement has been detected.""" - if cid in self.last_event: - if self.last_event[cid]["type"] == "movement": - if self.last_event[cid][ + if camera_id in self.last_event: + if self.last_event[camera_id]["type"] == "movement": + if self.last_event[camera_id][ "video_status" - ] == "recording" and self.last_event[cid]["time"] + offset > int( + ] == "recording" and self.last_event[camera_id]["time"] + offset > int( time.time() ): return True return False - def human_detected(self, cid: str, offset: int = 0) -> bool: + def human_detected(self, camera_id: str, offset: int = 0) -> bool: """Evaluate if a human has been detected.""" - if self.outdoor_last_event[cid]["video_status"] == "recording": - for e in self.outdoor_last_event[cid]["event_list"]: - if e["type"] == "human" and e["time"] + offset > int(time.time()): + if self.outdoor_last_event[camera_id]["video_status"] == "recording": + for event in self.outdoor_last_event[camera_id]["event_list"]: + if event["type"] == "human" and event["time"] + offset > int( + time.time() + ): return True return False - def animal_detected(self, cid: str, offset: int = 0) -> bool: + def animal_detected(self, camera_id: str, offset: int = 0) -> bool: """Evaluate if an animal has been detected.""" - if self.outdoor_last_event[cid]["video_status"] == "recording": - for e in self.outdoor_last_event[cid]["event_list"]: - if e["type"] == "animal" and e["time"] + offset > int(time.time()): + if self.outdoor_last_event[camera_id]["video_status"] == "recording": + for event in self.outdoor_last_event[camera_id]["event_list"]: + if event["type"] == "animal" and event["time"] + offset > int( + time.time() + ): return True return False - def car_detected(self, cid: str, offset: int = 0) -> bool: + def car_detected(self, camera_id: str, offset: int = 0) -> bool: """Evaluate if a car has been detected.""" - if self.outdoor_last_event[cid]["video_status"] == "recording": - for e in self.outdoor_last_event[cid]["event_list"]: - if e["type"] == "vehicle" and e["time"] + offset > int(time.time()): + if self.outdoor_last_event[camera_id]["video_status"] == "recording": + for event in self.outdoor_last_event[camera_id]["event_list"]: + if event["type"] == "vehicle" and event["time"] + offset > int( + time.time() + ): return True return False - def module_motion_detected(self, mid: str, cid: str, exclude: int = 0) -> bool: + def module_motion_detected( + self, module_id: str, camera_id: str, exclude: int = 0 + ) -> bool: """Evaluate if movement has been detected.""" if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events.get(cid, []), reverse=True) + array_time_event = sorted(self.events.get(camera_id, []), reverse=True) for time_ev in array_time_event: if time_ev < limit: return False if ( - self.events[cid][time_ev]["type"] == "tag_big_move" - or self.events[cid][time_ev]["type"] == "tag_small_move" - ) and self.events[cid][time_ev]["module_id"] == mid: + self.events[camera_id][time_ev]["type"] == "tag_big_move" + or self.events[camera_id][time_ev]["type"] == "tag_small_move" + ) and self.events[camera_id][time_ev]["module_id"] == module_id: return True elif ( - cid in self.last_event + camera_id in self.last_event and ( - self.last_event[cid]["type"] == "tag_big_move" - or self.last_event[cid]["type"] == "tag_small_move" + self.last_event[camera_id]["type"] == "tag_big_move" + or self.last_event[camera_id]["type"] == "tag_small_move" ) - and self.last_event[cid]["module_id"] == mid + and self.last_event[camera_id]["module_id"] == module_id ): return True return False - def module_opened(self, mid: str, cid: str, exclude: int = 0) -> bool: + def module_opened(self, module_id: str, camera_id: str, exclude: int = 0) -> bool: """Evaluate if module status is open.""" if exclude: limit = time.time() - exclude - array_time_event = sorted(self.events.get(cid, []), reverse=True) + array_time_event = sorted(self.events.get(camera_id, []), reverse=True) for time_ev in array_time_event: if time_ev < limit: return False if ( - self.events[cid][time_ev]["type"] == "tag_open" - and self.events[cid][time_ev]["module_id"] == mid + self.events[camera_id][time_ev]["type"] == "tag_open" + and self.events[camera_id][time_ev]["module_id"] == module_id ): return True - elif cid in self.last_event and ( - self.last_event[cid]["type"] == "tag_open" - and self.last_event[cid]["module_id"] == mid + elif camera_id in self.last_event and ( + self.last_event[camera_id]["type"] == "tag_open" + and self.last_event[camera_id]["module_id"] == module_id ): return True return False diff --git a/src/pyatmo/helpers.py b/src/pyatmo/helpers.py index f440ba50b..2fcb6b5f7 100644 --- a/src/pyatmo/helpers.py +++ b/src/pyatmo/helpers.py @@ -33,9 +33,9 @@ def today_stamps() -> Tuple[int, int]: return today, today + 3600 * 24 -def fix_id(rawData: Dict) -> Dict: - if rawData: - for station in rawData: +def fix_id(raw_data: Dict) -> Dict: + if raw_data: + for station in raw_data: station["_id"] = station["_id"].replace(" ", "") for module in station.get("modules", {}): module["_id"] = module["_id"].replace(" ", "") diff --git a/src/pyatmo/home_coach.py b/src/pyatmo/home_coach.py index 8aa354a0d..91ef55d04 100644 --- a/src/pyatmo/home_coach.py +++ b/src/pyatmo/home_coach.py @@ -1,8 +1,8 @@ -from .auth import NetatmOAuth2 +from .auth import NetatmoOAuth2 from .helpers import _BASE_URL from .weather_station import WeatherStationData -_GETHOMECOACHDATA_REQ = BASE_URL + "api/gethomecoachsdata" +_GETHOMECOACHDATA_REQ = _BASE_URL + "api/gethomecoachsdata" class HomeCoachData(WeatherStationData): @@ -10,11 +10,11 @@ class HomeCoachData(WeatherStationData): Class of Netatmo Home Couch devices (stations and modules) """ - def __init__(self, auth: NetatmOAuth2) -> None: + def __init__(self, auth: NetatmoOAuth2) -> None: """Initialize self. Arguments: - auth {NetatmOAuth2} -- Authentication information with a valid access token + auth {NetatmoOAuth2} -- Authentication information with a valid access token Raises: NoDevice: No devices found. diff --git a/src/pyatmo/public_data.py b/src/pyatmo/public_data.py index d7b9c0d60..6c96d21ae 100644 --- a/src/pyatmo/public_data.py +++ b/src/pyatmo/public_data.py @@ -1,6 +1,6 @@ from typing import Any, Dict -from .auth import NetatmOAuth2 +from .auth import NetatmoOAuth2 from .exceptions import NoDevice from .helpers import _BASE_URL, to_time_string @@ -28,7 +28,7 @@ class PublicData: def __init__( self, - auth: NetatmOAuth2, + auth: NetatmoOAuth2, LAT_NE: str, LON_NE: str, LAT_SW: str, @@ -39,7 +39,7 @@ def __init__( """Initialize self. Arguments: - auth {NetatmOAuth2} -- Authentication information with a valid access token + auth {NetatmoOAuth2} -- Authentication information with a valid access token LAT_NE {str} -- Latitude of the north east corner of the requested area. (-85 <= LAT_NE <= 85 and LAT_NE > LAT_SW) LON_NE {str} -- Longitude of the north east corner of the requested area. (-180 <= LON_NE <= 180 and LON_NE > LON_SW) LAT_SW {str} -- latitude of the south west corner of the requested area. (-85 <= LAT_SW <= 85) @@ -52,7 +52,7 @@ def __init__( NoDevice: No devices found. """ self.auth = auth - postParams: Dict = { + post_params: Dict = { "lat_ne": LAT_NE, "lon_ne": LON_NE, "lat_sw": LAT_SW, @@ -63,7 +63,7 @@ def __init__( if required_data_type: post_params["required_data"] = required_data_type - resp = self.auth.post_request(url=_GETPUBLIC_DATA, params=postParams) + resp = self.auth.post_request(url=_GETPUBLIC_DATA, params=post_params) try: self.raw_data = resp["body"] except (KeyError, TypeError): diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index c199d6fec..c4a858e12 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -1,18 +1,18 @@ import logging from typing import Dict -from .auth import NetatmOAuth2 +from .auth import NetatmoOAuth2 from .exceptions import InvalidRoom, NoDevice, NoSchedule from .helpers import _BASE_URL LOG = logging.getLogger(__name__) -_GETHOMESDATA_REQ = BASE_URL + "api/homesdata" -_GETHOMESTATUS_REQ = BASE_URL + "api/homestatus" -_SETTHERMMODE_REQ = BASE_URL + "api/setthermmode" -_SETROOMTHERMPOINT_REQ = BASE_URL + "api/setroomthermpoint" -_GETROOMMEASURE_REQ = BASE_URL + "api/getroommeasure" -_SWITCHHOMESCHEDULE_REQ = BASE_URL + "api/switchhomeschedule" +_GETHOMESDATA_REQ = _BASE_URL + "api/homesdata" +_GETHOMESTATUS_REQ = _BASE_URL + "api/homestatus" +_SETTHERMMODE_REQ = _BASE_URL + "api/setthermmode" +_SETROOMTHERMPOINT_REQ = _BASE_URL + "api/setroomthermpoint" +_GETROOMMEASURE_REQ = _BASE_URL + "api/getroommeasure" +_SWITCHHOMESCHEDULE_REQ = _BASE_URL + "api/switchhomeschedule" class HomeData: @@ -20,11 +20,11 @@ class HomeData: Class of Netatmo energy devices (relays, thermostat modules and valves) """ - def __init__(self, auth: NetatmOAuth2) -> None: + def __init__(self, auth: NetatmoOAuth2) -> None: """Initialize self. Arguments: - auth {NetatmOAuth2} -- Authentication information with a valid access token + auth {NetatmoOAuth2} -- Authentication information with a valid access token Raises: NoDevice: No devices found. @@ -34,11 +34,11 @@ def __init__(self, auth: NetatmOAuth2) -> None: if resp is None or "body" not in resp: raise NoDevice("No thermostat data returned by Netatmo server") - self.rawData = resp["body"].get("homes") - if not self.rawData: + self.raw_data = resp["body"].get("homes") + if not self.raw_data: raise NoDevice("No thermostat data available") - self.homes: Dict = {d["id"]: d for d in self.rawData} + self.homes: Dict = {d["id"]: d for d in self.raw_data} self.modules: Dict = {} self.rooms: Dict = {} @@ -46,17 +46,17 @@ def __init__(self, auth: NetatmOAuth2) -> None: self.zones: Dict = {} self.setpoint_duration: Dict = {} - for item in self.rawData: + for item in self.raw_data: home_id = item.get("id") - nameHome = item.get("name") - if not nameHome: - nameHome = "Unknown" - self.homes[home_id]["name"] = nameHome + home_name = item.get("name") + if not home_name: + home_name = "Unknown" + self.homes[home_id]["name"] = home_name if "modules" in item: if home_id not in self.modules: self.modules[home_id] = {} - for m in item["modules"]: - self.modules[home_id][m["id"]] = m + for module in item["modules"]: + self.modules[home_id][module["id"]] = module if home_id not in self.rooms: self.rooms[home_id] = {} if home_id not in self.schedules: @@ -76,15 +76,15 @@ def __init__(self, auth: NetatmOAuth2) -> None: for schedule in item["therm_schedules"]: self.schedules[home_id][schedule["id"]] = schedule for schedule in item["therm_schedules"]: - scheduleId = schedule["id"] - if scheduleId not in self.zones[home_id]: - self.zones[home_id][scheduleId] = {} + schedule_id = schedule["id"] + if schedule_id not in self.zones[home_id]: + self.zones[home_id][schedule_id] = {} for zone in schedule["zones"]: - self.zones[home_id][scheduleId][zone["id"]] = zone + self.zones[home_id][schedule_id][zone["id"]] = zone def _get_selected_schedule(self, home_id: str): """Get the selected schedule for a given home ID.""" - for key, value in self.schedules.get(home_id, {}).items(): + for value in self.schedules.get(home_id, {}).values(): if "selected" in value.keys(): return value return {} @@ -98,20 +98,20 @@ def switch_home_schedule(self, home_id: str, schedule_id: str): if schedule_id not in list(schedules.values()): raise NoSchedule("%s is not a valid schedule id" % schedule_id) - postParams = { + post_params = { "home_id": home_id, "schedule_id": schedule_id, } - resp = self.auth.post_request(url=_SWITCHHOMESCHEDULE_REQ, params=postParams) + resp = self.auth.post_request(url=_SWITCHHOMESCHEDULE_REQ, params=post_params) LOG.debug("Response: %s", resp) def get_hg_temp(self, home_id: str) -> float: """Return frost guard temperature value.""" - return self.get_selected_schedule(home_id).get("hg_temp") + return self._get_selected_schedule(home_id).get("hg_temp") def get_away_temp(self, home_id: str) -> float: """Return the configured away temperature value.""" - return self.get_selected_schedule(home_id).get("away_temp") + return self._get_selected_schedule(home_id).get("away_temp") def get_thermostat_type(self, home_id: str, room_id: str): """Return the thermostat type of the room.""" @@ -125,9 +125,9 @@ def __init__(self, auth, home_id): self.auth = auth self.home_id = home_id - postParams = {"home_id": self.home_id} + post_params = {"home_id": self.home_id} - resp = self.auth.post_request(url=_GETHOMESTATUS_REQ, params=postParams) + resp = self.auth.post_request(url=_GETHOMESTATUS_REQ, params=post_params) if ( "errors" in resp or "body" not in resp @@ -137,16 +137,16 @@ def __init__(self, auth, home_id): LOG.error("Errors in response: %s", resp) raise NoDevice("No device found, errors in response") - self.rawData = resp["body"]["home"] + self.raw_data = resp["body"]["home"] self.rooms = {} self.thermostats = {} self.valves = {} self.relays = {} - for r in self.rawData.get("rooms", []): - self.rooms[r["id"]] = r + for room in self.raw_data.get("rooms", []): + self.rooms[room["id"]] = room - for module in self.rawData.get("modules", []): + for module in self.raw_data.get("modules", []): if module["type"] == "NATherm1": thermostat_id = module["id"] if thermostat_id not in self.thermostats: @@ -158,10 +158,10 @@ def __init__(self, auth, home_id): self.valves[valve_id] = {} self.valves[valve_id] = module elif module["type"] == "NAPlug": - relayId = module["id"] - if relayId not in self.relays: - self.relays[relayId] = {} - self.relays[relayId] = module + relay_id = module["id"] + if relay_id not in self.relays: + self.relays[relay_id] = {} + self.relays[relay_id] = module def get_room(self, room_id): for key, value in self.rooms.items(): @@ -204,18 +204,18 @@ def boiler_status(self, module_id: str): return self.get_thermostat(module_id).get("boiler_status") def set_thermmode(self, mode, end_time=None, schedule_id=None): - postParams = { + post_params = { "home_id": self.home_id, "mode": mode, } if end_time is not None and mode in ("hg", "away"): post_params["endtime"] = end_time if schedule_id is not None and mode == "schedule": - postParams["schedule_id"] = schedule_id - return self.auth.post_request(url=_SETTHERMMODE_REQ, params=postParams) + post_params["schedule_id"] = schedule_id + return self.auth.post_request(url=_SETTHERMMODE_REQ, params=post_params) def set_room_thermpoint(self, room_id: str, mode: str, temp=None, end_time=None): - postParams = { + post_params = { "home_id": self.home_id, "room_id": room_id, "mode": mode, @@ -225,5 +225,5 @@ def set_room_thermpoint(self, room_id: str, mode: str, temp=None, end_time=None) if temp is not None: post_params["temp"] = temp if end_time is not None: - postParams["endtime"] = end_time - return self.auth.post_request(url=_SETROOMTHERMPOINT_REQ, params=postParams) + post_params["endtime"] = end_time + return self.auth.post_request(url=_SETROOMTHERMPOINT_REQ, params=post_params) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index d13f2a8db..d2464a09c 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -2,14 +2,14 @@ import time from typing import Dict, List -from .auth import NetatmOAuth2 +from .auth import NetatmoOAuth2 from .exceptions import NoDevice from .helpers import _BASE_URL, fix_id, today_stamps LOG = logging.getLogger(__name__) -_GETMEASURE_REQ = BASE_URL + "api/getmeasure" -_GETSTATIONDATA_REQ = BASE_URL + "api/getstationsdata" +_GETMEASURE_REQ = _BASE_URL + "api/getmeasure" +_GETSTATIONDATA_REQ = _BASE_URL + "api/getstationsdata" class WeatherStationData: @@ -17,11 +17,11 @@ class WeatherStationData: Class of Netatmo Weather Station devices (stations and modules) """ - def __init__(self, auth: NetatmOAuth2, url_req: str = None) -> None: + def __init__(self, auth: NetatmoOAuth2, url_req: str = None) -> None: """Initialize self. Arguments: - auth {NetatmOAuth2} -- Authentication information with a valid access token + auth {NetatmoOAuth2} -- Authentication information with a valid access token Raises: NoDevice: No devices found. @@ -34,31 +34,31 @@ def __init__(self, auth: NetatmOAuth2, url_req: str = None) -> None: if resp is None or "body" not in resp: raise NoDevice("No weather station data returned by Netatmo server") try: - self.rawData = fix_id(resp["body"].get("devices")) + self.raw_data = fix_id(resp["body"].get("devices")) except KeyError: LOG.debug("No in response %s", resp) raise NoDevice("No weather station data returned by Netatmo server") - if not self.rawData: + if not self.raw_data: raise NoDevice("No weather station available") - self.stations = {d["_id"]: d for d in self.rawData} + self.stations = {d["_id"]: d for d in self.raw_data} self.modules = {} - for item in self.rawData: + for item in self.raw_data: if "modules" not in item: item["modules"] = [item] - for m in item["modules"]: - if "module_name" not in m: - if m["type"] == "NHC": - m["module_name"] = m["station_name"] + for module in item["modules"]: + if "module_name" not in module: + if module["type"] == "NHC": + module["module_name"] = module["station_name"] else: continue - self.modules[m["_id"]] = m - self.modules[m["_id"]]["main_device"] = item["_id"] + self.modules[module["_id"]] = module + self.modules[module["_id"]]["main_device"] = item["_id"] self.default_station = list(self.stations.values())[0]["station_name"] @@ -72,9 +72,9 @@ def get_module_names(self, station_id: str) -> List: return [] res.add(station_data.get("module_name", station_data.get("type"))) - for m in station_data["modules"]: + for module in station_data["modules"]: # Add module name, use module type if no name is available - res.add(m.get("module_name", m.get("type"))) + res.add(module.get("module_name", module.get("type"))) return list(res) @@ -105,17 +105,9 @@ def get_station(self, station_id: str) -> Dict: """Return station by id.""" return self.stations.get(station_id, {}) - def get_module(self, mid, sid=None): + def get_module(self, module_id: str): """Return module by id.""" - station = self.get_station(sid) if sid else None - if mid in self.modules: - if station: - for module in station["modules"]: - if module["_id"] == mid: - return module - else: - return self.modules[mid] - return {} + return self.modules.get(module_id, {}) def get_monitored_conditions(self, module_id: str) -> List: """Return monitored conditions for given module(s).""" @@ -161,32 +153,32 @@ def get_last_data(self, station_id=None, exclude=0): key = "_id" # Breaking change from Netatmo : dashboard_data no longer available if station lost - lastD = {} - s = self.get_station(station_id) + last_data = {} + station = self.get_station(station_id) - if not s or "dashboard_data" not in s: + if not station or "dashboard_data" not in station: LOG.debug("No dashboard data for station %s", station_id) - return lastD + return last_data # Define oldest acceptable sensor measure event limit = (time.time() - exclude) if exclude else 0 - ds = s["dashboard_data"] - if key in s and ds["time_utc"] > limit: - lastD[s[key]] = ds.copy() - lastD[s[key]]["When"] = lastD[s[key]].pop("time_utc") - lastD[s[key]]["wifi_status"] = s["wifi_status"] - lastD[s[key]]["reachable"] = s["reachable"] + data = station["dashboard_data"] + if key in station and data["time_utc"] > limit: + last_data[station[key]] = data.copy() + last_data[station[key]]["When"] = last_data[station[key]].pop("time_utc") + last_data[station[key]]["wifi_status"] = station["wifi_status"] + last_data[station[key]]["reachable"] = station["reachable"] - for module in s["modules"]: + for module in station["modules"]: if "dashboard_data" not in module or key not in module: continue - ds = module["dashboard_data"] - if "time_utc" in ds and ds["time_utc"] > limit: - lastD[module[key]] = ds.copy() - lastD[module[key]]["When"] = lastD[module[key]].pop("time_utc") + data = module["dashboard_data"] + if "time_utc" in data and data["time_utc"] > limit: + last_data[module[key]] = data.copy() + last_data[module[key]]["When"] = last_data[module[key]].pop("time_utc") # For potential use, add battery and radio coverage information to module data if present for i in ( @@ -197,26 +189,26 @@ def get_last_data(self, station_id=None, exclude=0): "wifi_status", ): if i in module: - lastD[module[key]][i] = module[i] + last_data[module[key]][i] = module[i] - return lastD + return last_data def check_not_updated(self, station_id: str, delay: int = 3600): """Check if a given station has not been updated.""" res = self.get_last_data(station_id) ret = [] - for mn, v in res.items(): - if time.time() - v["When"] > delay: - ret.append(mn) + for key, value in res.items(): + if time.time() - value["When"] > delay: + ret.append(key) return ret def check_updated(self, station_id: str, delay: int = 3600): """Check if a given station has been updated.""" res = self.get_last_data(station_id) ret = [] - for mn, v in res.items(): - if time.time() - v["When"] < delay: - ret.append(mn) + for key, value in res.items(): + if time.time() - value["When"] < delay: + ret.append(key) return ret def get_measure( @@ -232,26 +224,26 @@ def get_measure( real_time=False, ): """Retrieve data from a device or module.""" - postParams = {"device_id": device_id} + post_params = {"device_id": device_id} if module_id: - postParams["module_id"] = module_id + post_params["module_id"] = module_id - postParams["scale"] = scale - postParams["type"] = mtype + post_params["scale"] = scale + post_params["type"] = mtype if date_begin: - postParams["date_begin"] = date_begin + post_params["date_begin"] = date_begin if date_end: - postParams["date_end"] = date_end + post_params["date_end"] = date_end if limit: - postParams["limit"] = limit + post_params["limit"] = limit - postParams["optimize"] = "true" if optimize else "false" - postParams["real_time"] = "true" if real_time else "false" + post_params["optimize"] = "true" if optimize else "false" + post_params["real_time"] = "true" if real_time else "false" - return self.auth.post_request(url=_GETMEASURE_REQ, params=postParams) + return self.auth.post_request(url=_GETMEASURE_REQ, params=post_params) def get_min_max_t_h( self, station_id: str, module_id: str = None, frame: str = "last24" @@ -284,6 +276,6 @@ def get_min_max_t_h( ) if resp: - T = [v[0] for v in resp["body"].values()] - H = [v[1] for v in resp["body"].values()] - return min(T), max(T), min(H), max(H) + temperature = [temp[0] for temp in resp["body"].values()] + humidity = [hum[1] for hum in resp["body"].values()] + return min(temperature), max(temperature), min(humidity), max(humidity) diff --git a/tests/conftest.py b/tests/conftest.py index 46ae4a709..ab7141506 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,9 +45,9 @@ def home_data(auth, requests_mock): @pytest.fixture(scope="function") -def homeStatus(auth, home_id, requests_mock): - with open("fixtures/home_status_simple.json") as f: - json_fixture = json.load(f) +def home_status(auth, home_id, requests_mock): + with open("fixtures/home_status_simple.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESTATUS_REQ, json=json_fixture, @@ -113,16 +113,16 @@ def camera_home_data(auth, requests_mock): f"6d278460699e56180d47ab47169efb31/" f"MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTT{index},," ) - with open("fixtures/camera_ping.json") as f: - json_fixture = json.load(f) + with open("fixtures/camera_ping.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( vpn_url + "/command/ping", json=json_fixture, headers={"content-type": "application/json"}, ) local_url = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" - with open("fixtures/camera_ping.json") as f: - json_fixture = json.load(f) + with open("fixtures/camera_ping.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( local_url + "/command/ping", json=json_fixture, diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index be0d12150..f608c246b 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -38,22 +38,22 @@ def test_client_auth_invalid(requests_mock): def test_post_request_json(auth, requests_mock): """Test wrapper for posting requests against the Netatmo API.""" requests_mock.post( - pyatmo.helpers.BASE_URL, + pyatmo.helpers._BASE_URL, json={"a": "b"}, headers={"content-type": "application/json"}, ) - resp = auth.post_request(pyatmo.helpers.BASE_URL, None) + resp = auth.post_request(pyatmo.helpers._BASE_URL, None) assert resp == {"a": "b"} def test_post_request_binary(auth, requests_mock): """Test wrapper for posting requests against the Netatmo API.""" requests_mock.post( - pyatmo.helpers.BASE_URL, + pyatmo.helpers._BASE_URL, text="Success", headers={"content-type": "application/text"}, ) - resp = auth.post_request(pyatmo.helpers.BASE_URL, None) + resp = auth.post_request(pyatmo.helpers._BASE_URL, None) assert resp == b"Success" @@ -62,14 +62,14 @@ def test_post_request_binary(auth, requests_mock): ) def test_post_request_fail(auth, requests_mock, test_input, expected): """Test failing requests against the Netatmo API.""" - requests_mock.post(pyatmo.helpers.BASE_URL, status_code=test_input) + requests_mock.post(pyatmo.helpers._BASE_URL, status_code=test_input) if test_input == 200: - resp = auth.post_request(pyatmo.helpers.BASE_URL, None) + resp = auth.post_request(pyatmo.helpers._BASE_URL, None) assert resp is expected else: with pytest.raises(pyatmo.ApiError): - resp = auth.post_request(pyatmo.helpers.BASE_URL, None) + resp = auth.post_request(pyatmo.helpers._BASE_URL, None) @pytest.mark.parametrize( diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 9e3ba3a86..02d1d96c5 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -10,13 +10,13 @@ from .conftest import does_not_raise -def test_CameraData(cameraHomeData): - assert cameraHomeData.homes is not None +def test_camera_data(camera_home_data): + assert camera_home_data.homes is not None -def test_HomeData_no_body(auth, requests_mock): - with open("fixtures/camera_data_empty.json") as f: - json_fixture = json.load(f) +def test_home_data_no_body(auth, requests_mock): + with open("fixtures/camera_data_empty.json") as fixture_file: + json_fixture = json.load(fixture_file) requests_mock.post( pyatmo.camera._GETHOMEDATA_REQ, json=json_fixture, @@ -26,9 +26,9 @@ def test_HomeData_no_body(auth, requests_mock): assert pyatmo.CameraData(auth) -def test_HomeData_no_homes(auth, requests_mock): - with open("fixtures/camera_home_data_no_homes.json") as f: - json_fixture = json.load(f) +def test_home_data_no_homes(auth, requests_mock): + with open("fixtures/camera_home_data_no_homes.json") as fixture_file: + json_fixture = json.load(fixture_file) requests_mock.post( pyatmo.camera._GETHOMEDATA_REQ, json=json_fixture, @@ -47,16 +47,16 @@ def test_HomeData_no_homes(auth, requests_mock): (None, None), ], ) -def test_CameraData_get_camera(cameraHomeData, cid, expected): - camera = cameraHomeData.get_camera(cid) +def test_camera_data_get_camera(camera_home_data, cid, expected): + camera = camera_home_data.get_camera(cid) assert camera.get("name") == expected -def test_CameraData_get_module(cameraHomeData): - assert cameraHomeData.get_module("00:00:00:00:00:00") is None +def test_camera_data_get_module(camera_home_data): + assert camera_home_data.get_module("00:00:00:00:00:00") is None -def test_CameraData_camera_urls(cameraHomeData, requests_mock): +def test_camera_data_camera_urls(camera_home_data, requests_mock): cid = "12:34:56:00:f1:62" vpn_url = ( "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/" @@ -79,25 +79,25 @@ def test_CameraData_camera_urls(cameraHomeData, requests_mock): headers={"content-type": "application/json"}, ) - cameraHomeData.update_camera_urls(cid) + camera_home_data.update_camera_urls(cid) - assert cameraHomeData.camera_urls(cid) == (vpn_url, local_url) + assert camera_home_data.camera_urls(cid) == (vpn_url, local_url) -def test_CameraData_update_camera_urls_empty(cameraHomeData): +def test_camera_data_update_camera_urls_empty(camera_home_data): camera_id = "12:34:56:00:f1:62" home_id = "91763b24c43d3e344f424e8b" - cameraHomeData.cameras[home_id][camera_id]["vpn_url"] = None - cameraHomeData.cameras[home_id][camera_id]["local_url"] = None + camera_home_data.cameras[home_id][camera_id]["vpn_url"] = None + camera_home_data.cameras[home_id][camera_id]["local_url"] = None - cameraHomeData.update_camera_urls(camera_id) + camera_home_data.update_camera_urls(camera_id) - assert cameraHomeData.camera_urls(camera_id) == (None, None) + assert camera_home_data.camera_urls(camera_id) == (None, None) -def test_CameraData_camera_urls_disconnected(auth, requests_mock): - with open("fixtures/camera_home_data_disconnected.json") as f: - json_fixture = json.load(f) +def test_camera_data_camera_urls_disconnected(auth, requests_mock): + with open("fixtures/camera_home_data_disconnected.json") as fixture_file: + json_fixture = json.load(fixture_file) requests_mock.post( pyatmo.camera._GETHOMEDATA_REQ, json=json_fixture, @@ -112,15 +112,15 @@ def test_CameraData_camera_urls_disconnected(auth, requests_mock): "MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTg,," ) local_url = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" - with open("fixtures/camera_ping.json") as f: - json_fixture = json.load(f) + with open("fixtures/camera_ping.json") as fixture_file: + json_fixture = json.load(fixture_file) requests_mock.post( vpn_url + "/command/ping", json=json_fixture, headers={"content-type": "application/json"}, ) - with open("fixtures/camera_ping.json") as f: - json_fixture = json.load(f) + with open("fixtures/camera_ping.json") as fixture_file: + json_fixture = json.load(fixture_file) requests_mock.post( local_url + "/command/ping", json=json_fixture, @@ -133,8 +133,8 @@ def test_CameraData_camera_urls_disconnected(auth, requests_mock): @pytest.mark.parametrize( "home_id, expected", [("91763b24c43d3e344f424e8b", ["Richard Doe"])], ) -def test_CameraData_persons_at_home(cameraHomeData, home_id, expected): - assert cameraHomeData.persons_at_home(home_id) == expected +def test_camera_data_persons_at_home(camera_home_data, home_id, expected): + assert camera_home_data.persons_at_home(home_id) == expected @freeze_time("2019-06-16") @@ -149,28 +149,32 @@ def test_CameraData_persons_at_home(cameraHomeData, home_id, expected): ("Jack Doe", "12:34:56:00:f1:62", None, False), ], ) -def test_CameraData_person_seen_by_camera(cameraHomeData, name, cid, exclude, expected): - assert cameraHomeData.person_seen_by_camera(name, cid, exclude=exclude) is expected +def test_camera_data_person_seen_by_camera( + camera_home_data, name, cid, exclude, expected +): + assert ( + camera_home_data.person_seen_by_camera(name, cid, exclude=exclude) is expected + ) -def test_CameraData__known_persons(cameraHomeData): - known_persons = cameraHomeData._known_persons() +def test_camera_data__known_persons(camera_home_data): + known_persons = camera_home_data._known_persons() assert len(known_persons) == 3 assert known_persons["91827374-7e04-5298-83ad-a0cb8372dff1"]["pseudo"] == "John Doe" -def test_CameraData_known_persons(cameraHomeData): - known_persons = cameraHomeData.known_persons() +def test_camera_data_known_persons(camera_home_data): + known_persons = camera_home_data.known_persons() assert len(known_persons) == 3 assert known_persons["91827374-7e04-5298-83ad-a0cb8372dff1"] == "John Doe" -def test_CameraData_known_persons_names(cameraHomeData): - assert sorted(cameraHomeData.known_persons_names()) == [ +def test_camera_data_known_persons_names(camera_home_data): + assert sorted(camera_home_data.known_persons_names()) == [ "Jane Doe", "John Doe", "Richard Doe", - } + ] @freeze_time("2019-06-16") @@ -182,8 +186,8 @@ def test_CameraData_known_persons_names(cameraHomeData): ("Dexter Foe", None), ], ) -def test_CameraData_get_person_id(cameraHomeData, name, expected): - assert cameraHomeData.get_person_id(name) == expected +def test_camera_data_get_person_id(camera_home_data, name, expected): + assert camera_home_data.get_person_id(name) == expected @pytest.mark.parametrize( @@ -203,8 +207,8 @@ def test_CameraData_get_person_id(cameraHomeData, name, expected): ), ], ) -def test_CameraData_set_persons_away( - cameraHomeData, requests_mock, hid, pid, json_fixture, expected +def test_camera_data_set_persons_away( + camera_home_data, requests_mock, home_id, person_id, json_fixture, expected ): with open("fixtures/%s" % json_fixture) as json_file: json_fixture = json.load(json_file) @@ -213,7 +217,7 @@ def test_CameraData_set_persons_away( json=json_fixture, headers={"content-type": "application/json"}, ) - assert cameraHomeData.set_persons_away(pid, hid)["status"] == expected + assert camera_home_data.set_persons_away(person_id, home_id)["status"] == expected @pytest.mark.parametrize( @@ -236,8 +240,8 @@ def test_CameraData_set_persons_away( ), ], ) -def test_CameraData_set_persons_home( - cameraHomeData, requests_mock, hid, pids, json_fixture, expected +def test_camera_data_set_persons_home( + camera_home_data, requests_mock, home_id, person_ids, json_fixture, expected ): with open("fixtures/%s" % json_fixture) as json_file: json_fixture = json.load(json_file) @@ -246,7 +250,7 @@ def test_CameraData_set_persons_home( json=json_fixture, headers={"content-type": "application/json"}, ) - assert cameraHomeData.set_persons_home(pids, hid)["status"] == expected + assert camera_home_data.set_persons_home(person_ids, home_id)["status"] == expected @freeze_time("2019-06-16") @@ -258,11 +262,11 @@ def test_CameraData_set_persons_home( (None, None, None, pytest.raises(pyatmo.NoDevice)), ], ) -def test_CameraData_someone_known_seen( - cameraHomeData, camera_id, exclude, expected, expectation +def test_camera_data_someone_known_seen( + camera_home_data, camera_id, exclude, expected, expectation ): with expectation: - assert cameraHomeData.someone_known_seen(camera_id, exclude) == expected + assert camera_home_data.someone_known_seen(camera_id, exclude) == expected @freeze_time("2019-06-16") @@ -274,11 +278,11 @@ def test_CameraData_someone_known_seen( (None, None, None, pytest.raises(pyatmo.NoDevice)), ], ) -def test_CameraData_someone_unknown_seen( - cameraHomeData, camera_id, exclude, expected, expectation +def test_camera_data_someone_unknown_seen( + camera_home_data, camera_id, exclude, expected, expectation ): with expectation: - assert cameraHomeData.someone_unknown_seen(camera_id, exclude) == expected + assert camera_home_data.someone_unknown_seen(camera_id, exclude) == expected @freeze_time("2019-06-16") @@ -291,11 +295,11 @@ def test_CameraData_someone_unknown_seen( (None, None, False, pytest.raises(pyatmo.NoDevice)), ], ) -def test_CameraData_motion_detected( - cameraHomeData, camera_id, exclude, expected, expectation +def test_camera_data_motion_detected( + camera_home_data, camera_id, exclude, expected, expectation ): with expectation: - assert cameraHomeData.motion_detected(camera_id, exclude) == expected + assert camera_home_data.motion_detected(camera_id, exclude) == expected @pytest.mark.parametrize( @@ -307,8 +311,8 @@ def test_CameraData_motion_detected( (None, None), ], ) -def test_CameraData_get_smokedetector(cameraHomeData, sid, expected): - smokedetector = cameraHomeData.get_smokedetector(sid) +def test_camera_data_get_smokedetector(camera_home_data, sid, expected): + smokedetector = camera_home_data.get_smokedetector(sid) if smokedetector: assert smokedetector["name"] == expected else: @@ -389,30 +393,30 @@ def test_camera_data_set_state( ) -def test_CameraData_get_light_state(cameraHomeData): +def test_camera_data_get_light_state(camera_home_data): camera_id = "12:34:56:00:a5:a4" expected = "auto" - assert cameraHomeData.get_light_state(camera_id) == expected + assert camera_home_data.get_light_state(camera_id) == expected -def test_CameraData_get_camera_picture(cameraHomeData, requests_mock): +def test_camera_data_get_camera_picture(camera_home_data, requests_mock): image_id = "5c22739723720a6e278c43bf" key = "276751836a6d1a71447f8d975494c87bc125766a970f7e022e79e001e021d756" - with open("fixtures/camera_image_sample.jpg", "rb") as f: - expect = f.read() + with open("fixtures/camera_image_sample.jpg", "rb") as fixture_file: + expect = fixture_file.read() requests_mock.post(pyatmo.camera._GETCAMERAPICTURE_REQ, content=expect) - assert cameraHomeData.get_camera_picture(image_id, key) == (expect, "jpeg") + assert camera_home_data.get_camera_picture(image_id, key) == (expect, "jpeg") -def test_CameraData_get_profile_image(cameraHomeData, requests_mock): - with open("fixtures/camera_image_sample.jpg", "rb") as f: - expect = f.read() +def test_camera_data_get_profile_image(camera_home_data, requests_mock): + with open("fixtures/camera_image_sample.jpg", "rb") as fixture_file: + expect = fixture_file.read() requests_mock.post(pyatmo.camera._GETCAMERAPICTURE_REQ, content=expect) - assert cameraHomeData.get_profile_image("John Doe") == (expect, "jpeg") - assert cameraHomeData.get_profile_image("Jack Foe") == (None, None) + assert camera_home_data.get_profile_image("John Doe") == (expect, "jpeg") + assert camera_home_data.get_profile_image("Jack Foe") == (None, None) @pytest.mark.parametrize( @@ -430,11 +434,11 @@ def test_CameraData_get_profile_image(cameraHomeData, requests_mock): ("91763b24c43d3e344f424e8b", None, "NSD", does_not_raise()), ], ) -def test_CameraData_update_event( - cameraHomeData, requests_mock, home_id, event_id, device_type, exception +def test_camera_data_update_event( + camera_home_data, requests_mock, home_id, event_id, device_type, exception ): - with open("fixtures/camera_data_events_until.json") as f: - json_fixture = json.load(f) + with open("fixtures/camera_data_events_until.json") as fixture_file: + json_fixture = json.load(fixture_file) requests_mock.post( pyatmo.camera._GETEVENTSUNTIL_REQ, json=json_fixture, @@ -442,46 +446,46 @@ def test_CameraData_update_event( ) with exception: assert ( - cameraHomeData.update_events( + camera_home_data.update_events( home_id=home_id, event_id=event_id, device_type=device_type ) is None ) -def test_CameraData_outdoor_motion_detected(cameraHomeData): +def test_camera_data_outdoor_motion_detected(camera_home_data): camera_id = "12:34:56:00:a5:a4" - assert cameraHomeData.outdoor_motion_detected(camera_id) is False - assert cameraHomeData.outdoor_motion_detected(camera_id, 100) is False + assert camera_home_data.outdoor_motion_detected(camera_id) is False + assert camera_home_data.outdoor_motion_detected(camera_id, 100) is False -def test_CameraData_human_detected(cameraHomeData): +def test_camera_data_human_detected(camera_home_data): camera_id = "12:34:56:00:a5:a4" - assert cameraHomeData.human_detected(camera_id) is False - assert cameraHomeData.human_detected(camera_id, 100) is False + assert camera_home_data.human_detected(camera_id) is False + assert camera_home_data.human_detected(camera_id, 100) is False -def test_CameraData_animal_detected(cameraHomeData): +def test_camera_data_animal_detected(camera_home_data): camera_id = "12:34:56:00:a5:a4" - assert cameraHomeData.animal_detected(camera_id) is False - assert cameraHomeData.animal_detected(camera_id, 100) is False + assert camera_home_data.animal_detected(camera_id) is False + assert camera_home_data.animal_detected(camera_id, 100) is False -def test_CameraData_car_detected(cameraHomeData): +def test_camera_data_car_detected(camera_home_data): camera_id = "12:34:56:00:a5:a4" - assert cameraHomeData.car_detected(camera_id) is False - assert cameraHomeData.car_detected(camera_id, 100) is False + assert camera_home_data.car_detected(camera_id) is False + assert camera_home_data.car_detected(camera_id, 100) is False -def test_CameraData_module_motion_detected(cameraHomeData): +def test_camera_data_module_motion_detected(camera_home_data): camera_id = "12:34:56:00:f1:62" module_id = "12:34:56:00:f2:f1" - assert cameraHomeData.module_motion_detected(camera_id, module_id) is False - assert cameraHomeData.module_motion_detected(camera_id, module_id, 100) is False + assert camera_home_data.module_motion_detected(camera_id, module_id) is False + assert camera_home_data.module_motion_detected(camera_id, module_id, 100) is False -def test_CameraData_module_opened(cameraHomeData): +def test_camera_data_module_opened(camera_home_data): camera_id = "12:34:56:00:f1:62" module_id = "12:34:56:00:f2:f1" - assert cameraHomeData.module_opened(camera_id, module_id) is False - assert cameraHomeData.module_opened(camera_id, module_id, 100) is False + assert camera_home_data.module_opened(camera_id, module_id) is False + assert camera_home_data.module_opened(camera_id, module_id, 100) is False diff --git a/tests/test_pyatmo_homecoach.py b/tests/test_pyatmo_homecoach.py index 514feb689..05fd840c5 100644 --- a/tests/test_pyatmo_homecoach.py +++ b/tests/test_pyatmo_homecoach.py @@ -24,8 +24,8 @@ def test_home_coach_data(home_coach_data): ), ], ) -def test_HomeCoachData_get_module_names(homeCoachData, station_id, expected): - assert sorted(homeCoachData.get_module_names(station_id)) == expected +def test_home_coach_data_get_module_names(home_coach_data, station_id, expected): + assert sorted(home_coach_data.get_module_names(station_id)) == expected @pytest.mark.parametrize( @@ -51,8 +51,8 @@ def test_HomeCoachData_get_module_names(homeCoachData, station_id, expected): ), ], ) -def test_HomeCoachData_get_modules(homeCoachData, station_id, expected): - assert homeCoachData.get_modules(station_id) == expected +def test_home_coach_data_get_modules(home_coach_data, station_id, expected): + assert home_coach_data.get_modules(station_id) == expected def test_home_coach_data_no_devices(auth, requests_mock): diff --git a/tests/test_pyatmo_publicdata.py b/tests/test_pyatmo_publicdata.py index 068653033..755ecb53f 100644 --- a/tests/test_pyatmo_publicdata.py +++ b/tests/test_pyatmo_publicdata.py @@ -21,13 +21,13 @@ def test_public_data(auth, requests_mock): headers={"content-type": "application/json"}, ) - publicData = pyatmo.PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW) - assert publicData.status == "ok" + public_data = pyatmo.PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW) + assert public_data.status == "ok" - publicData = pyatmo.PublicData( + public_data = pyatmo.PublicData( auth, LAT_NE, LON_NE, LAT_SW, LON_SW, required_data_type="temperature,rain_live" ) - assert publicData.status == "ok" + assert public_data.status == "ok" def test_public_data_unavailable(auth, requests_mock): @@ -48,53 +48,53 @@ def test_public_data_error(auth, requests_mock): pyatmo.PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW) -def test_PublicData_stations_in_area(publicData): - assert publicData.stations_in_area() == 8 +def test_public_data_stations_in_area(public_data): + assert public_data.stations_in_area() == 8 -def test_PublicData_get_latest_rain(publicData): +def test_public_data_get_latest_rain(public_data): expected = { "70:ee:50:1f:68:9e": 0, "70:ee:50:27:25:b0": 0, "70:ee:50:36:94:7c": 0.5, "70:ee:50:36:a9:fc": 0, } - assert publicData.get_latest_rain() == expected + assert public_data.get_latest_rain() == expected -def test_PublicData_get_average_rain(publicData): - assert publicData.get_average_rain() == 0.125 +def test_public_data_get_average_rain(public_data): + assert public_data.get_average_rain() == 0.125 -def test_PublicData_get_60_min_rain(publicData): +def test_public_data_get_60_min_rain(public_data): expected = { "70:ee:50:1f:68:9e": 0, "70:ee:50:27:25:b0": 0, "70:ee:50:36:94:7c": 0.2, "70:ee:50:36:a9:fc": 0, } - assert publicData.get_60_min_rain() == expected + assert public_data.get_60_min_rain() == expected -def test_PublicData_get_average_60_min_rain(publicData): - assert publicData.get_average_60_min_rain() == 0.05 +def test_public_data_get_average_60_min_rain(public_data): + assert public_data.get_average_60_min_rain() == 0.05 -def test_PublicData_get_24_h_rain(publicData): +def test_public_data_get_24_h_rain(public_data): expected = { "70:ee:50:1f:68:9e": 9.999, "70:ee:50:27:25:b0": 11.716000000000001, "70:ee:50:36:94:7c": 12.322000000000001, "70:ee:50:36:a9:fc": 11.009, } - assert publicData.get_24_h_rain() == expected + assert public_data.get_24_h_rain() == expected -def test_PublicData_get_average_24_h_rain(publicData): - assert publicData.get_average_24_h_rain() == 11.261500000000002 +def test_public_data_get_average_24_h_rain(public_data): + assert public_data.get_average_24_h_rain() == 11.261500000000002 -def test_PublicData_get_latest_pressures(publicData): +def test_public_data_get_latest_pressures(public_data): expected = { "70:ee:50:1f:68:9e": 1007.3, "70:ee:50:27:25:b0": 1012.8, @@ -105,14 +105,14 @@ def test_PublicData_get_latest_pressures(publicData): "70:ee:50:27:9f:2c": 1010.6, "70:ee:50:3c:02:78": 1011.7, } - assert publicData.get_latest_pressures() == expected + assert public_data.get_latest_pressures() == expected -def test_PublicData_get_average_pressure(publicData): - assert publicData.get_average_pressure() == 1010.3499999999999 +def test_public_data_get_average_pressure(public_data): + assert public_data.get_average_pressure() == 1010.3499999999999 -def test_PublicData_get_latest_temperatures(publicData): +def test_public_data_get_latest_temperatures(public_data): expected = { "70:ee:50:1f:68:9e": 21.1, "70:ee:50:27:25:b0": 23.2, @@ -123,14 +123,14 @@ def test_PublicData_get_latest_temperatures(publicData): "70:ee:50:27:9f:2c": 25.5, "70:ee:50:3c:02:78": 23.3, } - assert publicData.get_latest_temperatures() == expected + assert public_data.get_latest_temperatures() == expected -def test_PublicData_get_average_temperature(publicData): - assert publicData.get_average_temperature() == 22.725 +def test_public_data_get_average_temperature(public_data): + assert public_data.get_average_temperature() == 22.725 -def test_PublicData_get_latest_humidities(publicData): +def test_public_data_get_latest_humidities(public_data): expected = { "70:ee:50:1f:68:9e": 69, "70:ee:50:27:25:b0": 60, @@ -141,42 +141,42 @@ def test_PublicData_get_latest_humidities(publicData): "70:ee:50:27:9f:2c": 56, "70:ee:50:3c:02:78": 58, } - assert publicData.get_latest_humidities() == expected + assert public_data.get_latest_humidities() == expected -def test_PublicData_get_average_humidity(publicData): - assert publicData.get_average_humidity() == 63.25 +def test_public_data_get_average_humidity(public_data): + assert public_data.get_average_humidity() == 63.25 -def test_PublicData_get_latest_wind_strengths(publicData): +def test_public_data_get_latest_wind_strengths(public_data): expected = {"70:ee:50:36:a9:fc": 15} - assert publicData.get_latest_wind_strengths() == expected + assert public_data.get_latest_wind_strengths() == expected -def test_PublicData_get_average_wind_strength(publicData): - assert publicData.get_average_wind_strength() == 15 +def test_public_data_get_average_wind_strength(public_data): + assert public_data.get_average_wind_strength() == 15 -def test_PublicData_get_latest_wind_angles(publicData): +def test_public_data_get_latest_wind_angles(public_data): expected = {"70:ee:50:36:a9:fc": 17} - assert publicData.get_latest_wind_angles() == expected + assert public_data.get_latest_wind_angles() == expected -def test_PublicData_get_latest_gust_strengths(publicData): +def test_public_data_get_latest_gust_strengths(public_data): expected = {"70:ee:50:36:a9:fc": 31} - assert publicData.get_latest_gust_strengths() == expected + assert public_data.get_latest_gust_strengths() == expected -def test_PublicData_get_average_gust_strength(publicData): - assert publicData.get_average_gust_strength() == 31 +def test_public_data_get_average_gust_strength(public_data): + assert public_data.get_average_gust_strength() == 31 -def test_PublicData_get_latest_gust_angles(publicData): +def test_public_data_get_latest_gust_angles(public_data): expected = {"70:ee:50:36:a9:fc": 217} - assert publicData.get_latest_gust_angles() == expected + assert public_data.get_latest_gust_angles() == expected -def test_PublicData_get_locations(publicData): +def test_public_data_get_locations(public_data): expected = { "70:ee:50:1f:68:9e": [8.795445200000017, 50.2130169], "70:ee:50:27:25:b0": [8.7807159, 50.1946167], @@ -187,22 +187,22 @@ def test_PublicData_get_locations(publicData): "70:ee:50:27:9f:2c": [8.785342, 50.193573], "70:ee:50:3c:02:78": [8.795953681700666, 50.19530139868166], } - assert publicData.get_locations() == expected + assert public_data.get_locations() == expected -def test_PublicData_get_time_for_rain_measures(publicData): +def test_public_data_get_time_for_rain_measures(public_data): expected = { "70:ee:50:36:a9:fc": 1560248184, "70:ee:50:1f:68:9e": 1560248344, "70:ee:50:27:25:b0": 1560247896, "70:ee:50:36:94:7c": 1560248022, } - assert publicData.get_time_for_rain_measures() == expected + assert public_data.get_time_for_rain_measures() == expected -def test_PublicData_get_time_for_wind_measures(publicData): +def test_public_data_get_time_for_wind_measures(public_data): expected = {"70:ee:50:36:a9:fc": 1560248190} - assert publicData.get_time_for_wind_measures() == expected + assert public_data.get_time_for_wind_measures() == expected @pytest.mark.parametrize( @@ -249,8 +249,8 @@ def test_PublicData_get_time_for_wind_measures(publicData): ), ], ) -def test_PublicData_get_latest_station_measures(publicData, test_input, expected): - assert publicData.get_latest_station_measures(test_input) == expected +def test_public_data_get_latest_station_measures(public_data, test_input, expected): + assert public_data.get_latest_station_measures(test_input) == expected @pytest.mark.parametrize( @@ -263,8 +263,8 @@ def test_PublicData_get_latest_station_measures(publicData, test_input, expected ("wind_timeutc", {"70:ee:50:36:a9:fc": 1560248190}), ], ) -def test_PublicData_get_accessory_data(publicData, test_input, expected): - assert publicData.get_accessory_data(test_input) == expected +def test_public_data_get_accessory_data(public_data, test_input, expected): + assert public_data.get_accessory_data(test_input) == expected @pytest.mark.parametrize( @@ -299,5 +299,5 @@ def test_PublicData_get_accessory_data(publicData, test_input, expected): ({}, 0), ], ) -def test_PublicData_average(test_input, expected): +def test_public_data_average(test_input, expected): assert pyatmo.public_data.average(test_input) == expected diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index b3eb9a9d6..d9dd49d04 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -9,7 +9,7 @@ from tests.conftest import does_not_raise -def test_HomeData(homeData): +def test_home_data(home_data): expected = { "12:34:56:00:fa:d0": { "id": "12:34:56:00:fa:d0", @@ -54,7 +54,7 @@ def test_HomeData(homeData): "room_id": "3688132631", }, } - assert homeData.modules["91763b24c43d3e344f424e8b"] == expected + assert home_data.modules["91763b24c43d3e344f424e8b"] == expected def test_home_data_no_data(auth, requests_mock): @@ -75,9 +75,9 @@ def test_home_data_no_body(auth, requests_mock): assert pyatmo.HomeData(auth) -def test_HomeData_no_homes(auth, requests_mock): - with open("fixtures/home_data_no_homes.json") as f: - json_fixture = json.load(f) +def test_home_data_no_homes(auth, requests_mock): + with open("fixtures/home_data_no_homes.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, @@ -87,9 +87,9 @@ def test_HomeData_no_homes(auth, requests_mock): assert pyatmo.HomeData(auth) -def test_HomeData_no_home_name(auth, requests_mock): - with open("fixtures/home_data_nohomename.json") as f: - json_fixture = json.load(f) +def test_home_data_no_home_name(auth, requests_mock): + with open("fixtures/home_data_nohomename.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESDATA_REQ, json=json_fixture, @@ -97,22 +97,23 @@ def test_HomeData_no_home_name(auth, requests_mock): ) home_data = pyatmo.HomeData(auth) home_id = "91763b24c43d3e344f424e8b" - assert homeData.homes.get(home_id)["name"] == "Unknown" + assert home_data.homes.get(home_id)["name"] == "Unknown" @pytest.mark.parametrize( "home_id, expected", [("91763b24c43d3e344f424e8b", "MYHOME"), ("91763b24c43d3e344f424e8c", "Unknown")], ) -def test_HomeData_homes_by_id(homeData, home_id, expected): - assert homeData.homes[home_id]["name"] == expected +def test_home_data_homes_by_id(home_data, home_id, expected): + assert home_data.homes[home_id]["name"] == expected -def test_HomeData_get_selected_schedule(homeData): +def test_home_data_get_selected_schedule(home_data): assert ( - homeData.get_selected_schedule("91763b24c43d3e344f424e8b")["name"] == "Default" + home_data._get_selected_schedule("91763b24c43d3e344f424e8b")["name"] + == "Default" ) - assert homeData.get_selected_schedule("Unknown") == {} + assert home_data._get_selected_schedule("Unknown") == {} @pytest.mark.parametrize( @@ -126,8 +127,8 @@ def test_HomeData_get_selected_schedule(homeData): ), ], ) -def test_HomeData_switch_home_schedule( - homeData, requests_mock, t_home_id, t_sched_id, expected +def test_home_data_switch_home_schedule( + home_data, requests_mock, t_home_id, t_sched_id, expected ): with open("fixtures/status_ok.json") as json_file: json_fixture = json.load(json_file) @@ -137,23 +138,23 @@ def test_HomeData_switch_home_schedule( headers={"content-type": "application/json"}, ) with expected: - homeData.switch_home_schedule(home_id=t_home_id, schedule_id=t_sched_id) + home_data.switch_home_schedule(home_id=t_home_id, schedule_id=t_sched_id) @pytest.mark.parametrize( "home_id, expected", [("91763b24c43d3e344f424e8b", 14), ("00000000000000000000000", None)], ) -def test_HomeData_get_away_temp(homeData, home_id, expected): - assert homeData.get_away_temp(home_id) == expected +def test_home_data_get_away_temp(home_data, home_id, expected): + assert home_data.get_away_temp(home_id) == expected @pytest.mark.parametrize( "home_id, expected", [("91763b24c43d3e344f424e8b", 7), ("00000000000000000000000", None)], ) -def test_HomeData_get_hg_temp(homeData, home_id, expected): - assert homeData.get_hg_temp(home_id) == expected +def test_home_data_get_hg_temp(home_data, home_id, expected): + assert home_data.get_hg_temp(home_id) == expected @pytest.mark.parametrize( @@ -164,8 +165,8 @@ def test_HomeData_get_hg_temp(homeData, home_id, expected): ("91763b24c43d3e344f424e8b", "0000000000", None), ], ) -def test_HomeData_thermostat_type(homeData, home_id, module_id, expected): - assert homeData.get_thermostat_type(home_id, module_id) == expected +def test_home_data_thermostat_type(home_data, home_id, module_id, expected): + assert home_data.get_thermostat_type(home_id, module_id) == expected @pytest.mark.parametrize( @@ -186,9 +187,9 @@ def test_HomeData_thermostat_type(homeData, home_id, module_id, expected): ), ], ) -def test_HomeStatus(homeStatus, room_id, expected): - assert len(homeStatus.rooms) == 3 - assert homeStatus.rooms[room_id] == expected +def test_home_status(home_status, room_id, expected): + assert len(home_status.rooms) == 3 + assert home_status.rooms[room_id] == expected def test_home_status_error_and_data(auth, requests_mock): @@ -199,8 +200,8 @@ def test_home_status_error_and_data(auth, requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - homeStatus = pyatmo.HomeStatus(auth, home_id="91763b24c43d3e344f424e8b") - assert len(homeStatus.rooms) == 3 + home_status = pyatmo.HomeStatus(auth, home_id="91763b24c43d3e344f424e8b") + assert len(home_status.rooms) == 3 expexted = { "id": "2746182631", @@ -211,7 +212,7 @@ def test_home_status_error_and_data(auth, requests_mock): "therm_setpoint_start_time": 1559229567, "therm_setpoint_end_time": 0, } - assert homeStatus.rooms["2746182631"] == expexted + assert home_status.rooms["2746182631"] == expexted def test_home_status_error(auth, requests_mock): @@ -234,7 +235,7 @@ def test_home_status_error(auth, requests_mock): @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_get_room(homeStatus): +def test_home_status_get_room(home_status): expexted = { "id": "2746182631", "reachable": True, @@ -244,13 +245,13 @@ def test_HomeStatus_get_room(homeStatus): "therm_setpoint_start_time": 1559229567, "therm_setpoint_end_time": 0, } - assert homeStatus.get_room("2746182631") == expexted + assert home_status.get_room("2746182631") == expexted with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.get_room("0000000000") + assert home_status.get_room("0000000000") @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_get_thermostat(homeStatus): +def test_home_status_get_thermostat(home_status): expexted = { "id": "12:34:56:00:01:ae", "reachable": True, @@ -264,13 +265,13 @@ def test_HomeStatus_get_thermostat(homeStatus): "bridge": "12:34:56:00:fa:d0", "battery_state": "high", } - assert homeStatus.get_thermostat("12:34:56:00:01:ae") == expexted + assert home_status.get_thermostat("12:34:56:00:01:ae") == expexted with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.get_thermostat("00:00:00:00:00:00") + assert home_status.get_thermostat("00:00:00:00:00:00") @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_get_relay(homeStatus): +def test_home_status_get_relay(home_status): expexted = { "id": "12:34:56:00:fa:d0", "type": "NAPlug", @@ -278,13 +279,13 @@ def test_HomeStatus_get_relay(homeStatus): "rf_strength": 107, "wifi_strength": 42, } - assert homeStatus.get_relay("12:34:56:00:fa:d0") == expexted + assert home_status.get_relay("12:34:56:00:fa:d0") == expexted with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.get_relay("00:00:00:00:00:00") + assert home_status.get_relay("00:00:00:00:00:00") @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_get_valve(homeStatus): +def test_home_status_get_valve(home_status): expexted = { "id": "12:34:56:03:a5:54", "reachable": True, @@ -295,35 +296,35 @@ def test_HomeStatus_get_valve(homeStatus): "bridge": "12:34:56:00:fa:d0", "battery_state": "full", } - assert homeStatus.get_valve("12:34:56:03:a5:54") == expexted + assert home_status.get_valve("12:34:56:03:a5:54") == expexted with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.get_valve("00:00:00:00:00:00") + assert home_status.get_valve("00:00:00:00:00:00") @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_set_point(homeStatus): - assert homeStatus.set_point("2746182631") == 12 +def test_home_status_set_point(home_status): + assert home_status.set_point("2746182631") == 12 with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.set_point("0000000000") + assert home_status.set_point("0000000000") @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_set_point_mode(homeStatus): - assert homeStatus.set_point_mode("2746182631") == "away" +def test_home_status_set_point_mode(home_status): + assert home_status.set_point_mode("2746182631") == "away" with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.set_point_mode("0000000000") + assert home_status.set_point_mode("0000000000") @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_measured_temperature(homeStatus): - assert homeStatus.measured_temperature("2746182631") == 19.8 +def test_home_status_measured_temperature(home_status): + assert home_status.measured_temperature("2746182631") == 19.8 with pytest.raises(pyatmo.InvalidRoom): - assert homeStatus.measured_temperature("0000000000") + assert home_status.measured_temperature("0000000000") @pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_HomeStatus_boiler_status(homeStatus): - assert homeStatus.boiler_status("12:34:56:00:01:ae") is False +def test_home_status_boiler_status(home_status): + assert home_status.boiler_status("12:34:56:00:01:ae") is False @pytest.mark.parametrize( @@ -389,15 +390,8 @@ def test_HomeStatus_boiler_status(homeStatus): ), ], ) -def test_HomeStatus_set_thermmode( - homeStatus, - requests_mock, - home_id, - mode, - end_time, - schedule_id, - json_fixture, - expected, +def test_home_status_set_thermmode( + home_status, requests_mock, mode, end_time, schedule_id, json_fixture, expected, ): with open("fixtures/%s" % json_fixture) as json_file: json_fixture = json.load(json_file) @@ -406,7 +400,7 @@ def test_HomeStatus_set_thermmode( json=json_fixture, headers={"content-type": "application/json"}, ) - res = homeStatus.set_thermmode( + res = home_status.set_thermmode( mode=mode, end_time=end_time, schedule_id=schedule_id ) if "error" in res: @@ -456,16 +450,8 @@ def test_HomeStatus_set_thermmode( ), ], ) -def test_HomeStatus_set_room_thermpoint( - homeStatus, - requests_mock, - home_id, - room_id, - mode, - temp, - end_time, - json_fixture, - expected, +def test_home_status_set_room_thermpoint( + home_status, requests_mock, room_id, mode, temp, end_time, json_fixture, expected, ): with open("fixtures/%s" % json_fixture) as json_file: json_fixture = json.load(json_file) @@ -475,7 +461,7 @@ def test_HomeStatus_set_room_thermpoint( headers={"content-type": "application/json"}, ) assert ( - homeStatus.set_room_thermpoint( + home_status.set_room_thermpoint( room_id=room_id, mode=mode, temp=temp, end_time=end_time )["status"] == expected @@ -519,16 +505,8 @@ def test_HomeStatus_set_room_thermpoint( ), ], ) -def test_HomeStatus_set_room_thermpoint_error( - homeStatus, - requests_mock, - caplog, - home_id, - room_id, - mode, - temp, - json_fixture, - expected, +def test_home_status_set_room_thermpoint_error( + home_status, requests_mock, room_id, mode, temp, json_fixture, expected, ): with open("fixtures/%s" % json_fixture) as json_file: json_fixture = json.load(json_file) @@ -538,18 +516,18 @@ def test_HomeStatus_set_room_thermpoint_error( headers={"content-type": "application/json"}, ) assert ( - homeStatus.set_room_thermpoint(room_id=room_id, mode=mode, temp=temp)["error"][ + home_status.set_room_thermpoint(room_id=room_id, mode=mode, temp=temp)["error"][ "message" ] == expected ) -def test_HomeStatus_error_disconnected( +def test_home_status_error_disconnected( auth, requests_mock, home_id="91763b24c43d3e344f424e8b" ): - with open("fixtures/home_status_error_disconnected.json") as f: - json_fixture = json.load(f) + with open("fixtures/home_status_error_disconnected.json") as json_file: + json_fixture = json.load(json_file) requests_mock.post( pyatmo.thermostat._GETHOMESTATUS_REQ, json=json_fixture, diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 44ece3d5e..86ca502f3 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -66,8 +66,8 @@ def test_weather_station_data_no_data(auth, requests_mock): ), ], ) -def test_WeatherStationData_get_module_names(weatherStationData, station_id, expected): - assert sorted(weatherStationData.get_module_names(station_id)) == expected +def test_weather_station_get_module_names(weather_station_data, station_id, expected): + assert sorted(weather_station_data.get_module_names(station_id)) == expected @pytest.mark.parametrize( @@ -118,12 +118,12 @@ def test_WeatherStationData_get_module_names(weatherStationData, station_id, exp ), ], ) -def test_WeatherStationData_get_modules(weatherStationData, station_id, expected): - assert weatherStationData.get_modules(station_id) == expected +def test_weather_station_get_modules(weather_station_data, station_id, expected): + assert weather_station_data.get_modules(station_id) == expected -def test_WeatherStationData_get_station(weatherStationData): - result = weatherStationData.get_station("12:34:56:37:11:ca") +def test_weather_station_get_station(weather_station_data): + result = weather_station_data.get_station("12:34:56:37:11:ca") assert result["_id"] == "12:34:56:37:11:ca" assert result["station_name"] == "MyStation" @@ -137,23 +137,22 @@ def test_WeatherStationData_get_station(weatherStationData): "Pressure", ] - assert weatherStationData.get_station("NoValidStation") == {} + assert weather_station_data.get_station("NoValidStation") == {} @pytest.mark.parametrize( - "mid, sid, expected", + "mid, expected", [ - ("12:34:56:07:bb:3e", None, "12:34:56:07:bb:3e"), - ("12:34:56:07:bb:3e", "12:34:56:37:11:ca", "12:34:56:07:bb:3e"), - ("", None, {}), - ("", "", {}), - (None, None, {}), + ("12:34:56:07:bb:3e", "12:34:56:07:bb:3e"), + ("12:34:56:07:bb:3e", "12:34:56:07:bb:3e"), + ("", {}), + (None, {}), ], ) -def test_WeatherStationData_get_module(weatherStationData, mid, sid, expected): - mod = weatherStationData.get_module(mid, sid) +def test_weather_station_get_module(weather_station_data, mid, expected): + mod = weather_station_data.get_module(mid) - assert type(mod) == dict + assert isinstance(mod, dict) is True assert mod.get("_id", mod) == expected @@ -234,10 +233,10 @@ def test_WeatherStationData_get_module(weatherStationData, mid, sid, expected): ), ], ) -def test_WeatherStationData_get_monitored_conditions( - weatherStationData, module_id, expected +def test_weather_station_get_monitored_conditions( + weather_station_data, module_id, expected ): - assert sorted(weatherStationData.get_monitored_conditions(module_id)) == expected + assert sorted(weather_station_data.get_monitored_conditions(module_id)) == expected @freeze_time("2019-06-11") @@ -284,10 +283,10 @@ def test_WeatherStationData_get_monitored_conditions( ), ], ) -def test_WeatherStationData_get_last_data( - weatherStationData, station_id, exclude, expected +def test_weather_station_get_last_data( + weather_station_data, station_id, exclude, expected ): - mod = weatherStationData.get_last_data(station_id, exclude=exclude) + mod = weather_station_data.get_last_data(station_id, exclude=exclude) if mod: assert sorted(mod) == expected else: @@ -319,10 +318,10 @@ def test_WeatherStationData_get_last_data( ), ], ) -def test_WeatherStationData_check_not_updated( - weatherStationData, station_id, delay, expected +def test_weather_station_check_not_updated( + weather_station_data, station_id, delay, expected ): - mod = weatherStationData.check_not_updated(station_id, delay) + mod = weather_station_data.check_not_updated(station_id, delay) assert sorted(mod) == expected @@ -345,10 +344,10 @@ def test_WeatherStationData_check_not_updated( ("12:34:56:37:11:ca", 100, [],), ], ) -def test_WeatherStationData_check_updated( - weatherStationData, station_id, delay, expected +def test_weather_station_check_updated( + weather_station_data, station_id, delay, expected ): - mod = weatherStationData.check_updated(station_id, delay) + mod = weather_station_data.check_updated(station_id, delay) if mod: assert sorted(mod) == expected else: @@ -359,8 +358,8 @@ def test_WeatherStationData_check_updated( @pytest.mark.parametrize( "device_id, scale, mtype, expected", [("MyStation", "scale", "type", [28.1])] ) -def test_WeatherStationData_get_measure( - weatherStationData, requests_mock, device_id, scale, mtype, expected +def test_weather_station_get_measure( + weather_station_data, requests_mock, device_id, scale, mtype, expected ): with open("fixtures/weatherstation_measure.json") as json_file: json_fixture = json.load(json_file) @@ -370,16 +369,16 @@ def test_WeatherStationData_get_measure( headers={"content-type": "application/json"}, ) assert ( - weatherStationData.get_measure(device_id, scale, mtype)["body"]["1544558433"] + weather_station_data.get_measure(device_id, scale, mtype)["body"]["1544558433"] == expected ) -def test_WeatherStationData_get_last_data_measurements(weatherStationData): +def test_weather_station_get_last_data_measurements(weather_station_data): station_id = "12:34:56:37:11:ca" module_id = "12:34:56:03:1b:e4" - mod = weatherStationData.get_last_data(station_id, None) + mod = weather_station_data.get_last_data(station_id, None) assert mod[station_id]["min_temp"] == 23.4 assert mod[station_id]["max_temp"] == 25.6 @@ -411,10 +410,10 @@ def test_WeatherStationData_get_last_data_measurements(weatherStationData): ("12:34:56:00:aa:01", None, {},), ], ) -def test_WeatherStationData_get_last_data_bug_97( - weatherStationData, station_id, exclude, expected +def test_weather_station_get_last_data_bug_97( + weather_station_data, station_id, exclude, expected ): - mod = weatherStationData.get_last_data(station_id, exclude) + mod = weather_station_data.get_last_data(station_id, exclude) if mod: assert sorted(mod) == expected else: From 280124500b80fb50fd02b5e091e3c6e9ea46346d Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 2 Jun 2020 16:57:15 +0200 Subject: [PATCH 248/333] Add some typing --- src/pyatmo/auth.py | 2 +- src/pyatmo/camera.py | 6 ++--- src/pyatmo/thermostat.py | 2 +- src/pyatmo/weather_station.py | 36 ++++++++++++++--------------- tests/test_pyatmo_weatherstation.py | 10 ++++---- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index c046b5053..1a51fa992 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -122,7 +122,7 @@ def post_request( LOG.debug("Remote end closed connection without response (%s)", url) else: - def query(url, params, timeout, retries): + def query(url: str, params: Dict, timeout: int, retries: int) -> Any: if retries == 0: LOG.error("Too many retries") return diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 61f43386b..fe1ccd69f 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -341,17 +341,17 @@ def person_seen_by_camera(self, name, camera_id, exclude=0): return True return False - def _known_persons(self): + def _known_persons(self) -> Dict[str, Dict]: known_persons = {} for person_id, person in self.persons.items(): if "pseudo" in person: known_persons[person_id] = person return known_persons - def known_persons(self): + def known_persons(self) -> Dict[str, str]: return {pid: p["pseudo"] for pid, p in self._known_persons().items()} - def known_persons_names(self): + def known_persons_names(self) -> List[str]: names = [] for person in self._known_persons().values(): names.append(person["pseudo"]) diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index c4a858e12..439b40f81 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -163,7 +163,7 @@ def __init__(self, auth, home_id): self.relays[relay_id] = {} self.relays[relay_id] = module - def get_room(self, room_id): + def get_room(self, room_id: str) -> Dict: for key, value in self.rooms.items(): if value["id"] == room_id: return self.rooms[key] diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index d2464a09c..405bca8de 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -148,12 +148,12 @@ def get_monitored_conditions(self, module_id: str) -> List: conditions.append("reachable") return conditions - def get_last_data(self, station_id=None, exclude=0): + def get_last_data(self, station_id: str, exclude: int = 0) -> Dict: """Return data for a given station and time frame.""" key = "_id" # Breaking change from Netatmo : dashboard_data no longer available if station lost - last_data = {} + last_data: Dict = {} station = self.get_station(station_id) if not station or "dashboard_data" not in station: @@ -211,17 +211,17 @@ def check_updated(self, station_id: str, delay: int = 3600): ret.append(key) return ret - def get_measure( + def get_data( self, - device_id, - scale, - mtype, - module_id=None, - date_begin=None, - date_end=None, - limit=None, - optimize=False, - real_time=False, + device_id: str, + scale: str, + module_type: str, + module_id: str = None, + date_begin: float = None, + date_end: float = None, + limit: int = None, + optimize: bool = False, + real_time: bool = False, ): """Retrieve data from a device or module.""" post_params = {"device_id": device_id} @@ -229,16 +229,16 @@ def get_measure( post_params["module_id"] = module_id post_params["scale"] = scale - post_params["type"] = mtype + post_params["type"] = module_type if date_begin: - post_params["date_begin"] = date_begin + post_params["date_begin"] = f"{date_begin}" if date_end: - post_params["date_end"] = date_end + post_params["date_end"] = f"{date_end}" if limit: - post_params["limit"] = limit + post_params["limit"] = f"{limit}" post_params["optimize"] = "true" if optimize else "false" post_params["real_time"] = "true" if real_time else "false" @@ -266,11 +266,11 @@ def get_min_max_t_h( elif frame == "day": start, end = today_stamps() - resp = self.get_measure( + resp = self.get_data( device_id=station_id, module_id=module_id, scale="max", - mtype="Temperature,Humidity", + module_type="Temperature,Humidity", date_begin=start, date_end=end, ) diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 86ca502f3..875e256d4 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -356,10 +356,10 @@ def test_weather_station_check_updated( @freeze_time("2019-06-11") @pytest.mark.parametrize( - "device_id, scale, mtype, expected", [("MyStation", "scale", "type", [28.1])] + "device_id, scale, module_type, expected", [("MyStation", "scale", "type", [28.1])] ) -def test_weather_station_get_measure( - weather_station_data, requests_mock, device_id, scale, mtype, expected +def test_weather_station_get_data( + weather_station_data, requests_mock, device_id, scale, module_type, expected ): with open("fixtures/weatherstation_measure.json") as json_file: json_fixture = json.load(json_file) @@ -369,7 +369,9 @@ def test_weather_station_get_measure( headers={"content-type": "application/json"}, ) assert ( - weather_station_data.get_measure(device_id, scale, mtype)["body"]["1544558433"] + weather_station_data.get_data(device_id, scale, module_type)["body"][ + "1544558433" + ] == expected ) From c2013313329d3fd5dcc1bc775fcb166d889eaac7 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 2 Jun 2020 22:39:19 +0200 Subject: [PATCH 249/333] Add more type annotations --- src/pyatmo/camera.py | 16 +++++++---- src/pyatmo/thermostat.py | 53 +++++++++++++++++++---------------- src/pyatmo/weather_station.py | 14 +++++---- 3 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index fe1ccd69f..2617d2d7a 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -115,11 +115,11 @@ def get_camera(self, camera_id: str) -> Dict[str, str]: return self.cameras[home_id][camera_id] return {} - def get_module(self, module_id: str): + def get_module(self, module_id: str) -> Optional[dict]: """Get module data.""" return None if module_id not in self.modules else self.modules[module_id] - def get_smokedetector(self, smoke_id: str): + def get_smokedetector(self, smoke_id: str) -> Optional[dict]: """Get smoke detector.""" for home_id, _ in self.smokedetectors.items(): if smoke_id in self.smokedetectors[home_id]: @@ -220,7 +220,9 @@ def get_person_id(self, name: str) -> Optional[str]: return pid return None - def get_camera_picture(self, image_id: str, key: str): + def get_camera_picture( + self, image_id: str, key: str + ) -> Tuple[bytes, Optional[str]]: """Download a specific image (of an event or user face) from the camera.""" post_params = { "image_id": image_id, @@ -230,7 +232,7 @@ def get_camera_picture(self, image_id: str, key: str): image_type = imghdr.what("NONE.FILE", resp) return resp, image_type - def get_profile_image(self, name: str): + def get_profile_image(self, name: str) -> Tuple[Optional[bytes], Optional[str]]: """Retrieve the face of a given person.""" for person in self.persons: if "pseudo" in self.persons[person]: @@ -317,7 +319,9 @@ def update_events( sorted(self.outdoor_events[camera])[-1] ] - def person_seen_by_camera(self, name, camera_id, exclude=0): + def person_seen_by_camera( + self, name: str, camera_id: str, exclude: int = 0 + ) -> bool: """ Evaluate if a specific person has been seen """ @@ -401,7 +405,7 @@ def someone_unknown_seen(self, camera_id: str, exclude: int = 0) -> bool: return True return False - def motion_detected(self, camera_id, exclude=0): + def motion_detected(self, camera_id: str, exclude: int = 0) -> bool: """Evaluate if movement has been detected.""" if camera_id not in self.events: raise NoDevice diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 439b40f81..477853534 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -1,5 +1,5 @@ import logging -from typing import Dict +from typing import Any, Dict, Optional from .auth import NetatmoOAuth2 from .exceptions import InvalidRoom, NoDevice, NoSchedule @@ -82,15 +82,15 @@ def __init__(self, auth: NetatmoOAuth2) -> None: for zone in schedule["zones"]: self.zones[home_id][schedule_id][zone["id"]] = zone - def _get_selected_schedule(self, home_id: str): + def _get_selected_schedule(self, home_id: str) -> Dict: """Get the selected schedule for a given home ID.""" for value in self.schedules.get(home_id, {}).values(): if "selected" in value.keys(): return value return {} - def switch_home_schedule(self, home_id: str, schedule_id: str): - """.""" + def switch_home_schedule(self, home_id: str, schedule_id: str) -> Any: + """Switch the schedule for a give home ID.""" schedules = { self.schedules[home_id][s]["name"]: self.schedules[home_id][s]["id"] for s in self.schedules.get(home_id, {}) @@ -105,23 +105,24 @@ def switch_home_schedule(self, home_id: str, schedule_id: str): resp = self.auth.post_request(url=_SWITCHHOMESCHEDULE_REQ, params=post_params) LOG.debug("Response: %s", resp) - def get_hg_temp(self, home_id: str) -> float: + def get_hg_temp(self, home_id: str) -> Optional[float]: """Return frost guard temperature value.""" return self._get_selected_schedule(home_id).get("hg_temp") - def get_away_temp(self, home_id: str) -> float: + def get_away_temp(self, home_id: str) -> Optional[float]: """Return the configured away temperature value.""" return self._get_selected_schedule(home_id).get("away_temp") - def get_thermostat_type(self, home_id: str, room_id: str): + def get_thermostat_type(self, home_id: str, room_id: str) -> Optional[str]: """Return the thermostat type of the room.""" for module in self.modules.get(home_id, {}).values(): if module.get("room_id") == room_id: return module.get("type") + return None class HomeStatus: - def __init__(self, auth, home_id): + def __init__(self, auth: NetatmoOAuth2, home_id: str): self.auth = auth self.home_id = home_id @@ -138,10 +139,10 @@ def __init__(self, auth, home_id): raise NoDevice("No device found, errors in response") self.raw_data = resp["body"]["home"] - self.rooms = {} - self.thermostats = {} - self.valves = {} - self.relays = {} + self.rooms: Dict = {} + self.thermostats: Dict = {} + self.valves: Dict = {} + self.relays: Dict = {} for room in self.raw_data.get("rooms", []): self.rooms[room["id"]] = room @@ -169,52 +170,56 @@ def get_room(self, room_id: str) -> Dict: return self.rooms[key] raise InvalidRoom("No room with ID %s" % room_id) - def get_thermostat(self, room_id: str): + def get_thermostat(self, room_id: str) -> Dict: """Return thermostat data for a given room id.""" for key, value in self.thermostats.items(): if value["id"] == room_id: return self.thermostats[key] raise InvalidRoom("No room with ID %s" % room_id) - def get_relay(self, room_id: str): + def get_relay(self, room_id: str) -> Dict: for key, value in self.relays.items(): if value["id"] == room_id: return self.relays[key] raise InvalidRoom("No room with ID %s" % room_id) - def get_valve(self, room_id: str): + def get_valve(self, room_id: str) -> Dict: for key, value in self.valves.items(): if value["id"] == room_id: return self.valves[key] raise InvalidRoom("No room with ID %s" % room_id) - def set_point(self, room_id: str): + def set_point(self, room_id: str) -> Optional[float]: """Return the setpoint of a given room.""" return self.get_room(room_id).get("therm_setpoint_temperature") - def set_point_mode(self, room_id: str): + def set_point_mode(self, room_id: str) -> Optional[str]: """Return the setpointmode of a given room.""" return self.get_room(room_id).get("therm_setpoint_mode") - def measured_temperature(self, room_id: str): + def measured_temperature(self, room_id: str) -> Optional[float]: """Return the measured temperature of a given room.""" return self.get_room(room_id).get("therm_measured_temperature") - def boiler_status(self, module_id: str): + def boiler_status(self, module_id: str) -> Optional[bool]: return self.get_thermostat(module_id).get("boiler_status") - def set_thermmode(self, mode, end_time=None, schedule_id=None): + def set_thermmode( + self, mode: str, end_time: int = None, schedule_id: str = None + ) -> Optional[str]: post_params = { "home_id": self.home_id, "mode": mode, } if end_time is not None and mode in ("hg", "away"): - post_params["endtime"] = end_time + post_params["endtime"] = str(end_time) if schedule_id is not None and mode == "schedule": post_params["schedule_id"] = schedule_id return self.auth.post_request(url=_SETTHERMMODE_REQ, params=post_params) - def set_room_thermpoint(self, room_id: str, mode: str, temp=None, end_time=None): + def set_room_thermpoint( + self, room_id: str, mode: str, temp: float = None, end_time: int = None + ) -> Optional[str]: post_params = { "home_id": self.home_id, "room_id": room_id, @@ -223,7 +228,7 @@ def set_room_thermpoint(self, room_id: str, mode: str, temp=None, end_time=None) # Temp and endtime should only be send when mode=='manual', but netatmo api can # handle that even when mode == 'home' and these settings don't make sense if temp is not None: - post_params["temp"] = temp + post_params["temp"] = str(temp) if end_time is not None: - post_params["endtime"] = end_time + post_params["endtime"] = str(end_time) return self.auth.post_request(url=_SETROOMTHERMPOINT_REQ, params=post_params) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 405bca8de..c6d74691d 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -1,6 +1,6 @@ import logging import time -from typing import Dict, List +from typing import Dict, List, Optional, Tuple from .auth import NetatmoOAuth2 from .exceptions import NoDevice @@ -105,7 +105,7 @@ def get_station(self, station_id: str) -> Dict: """Return station by id.""" return self.stations.get(station_id, {}) - def get_module(self, module_id: str): + def get_module(self, module_id: str) -> Dict: """Return module by id.""" return self.modules.get(module_id, {}) @@ -193,7 +193,7 @@ def get_last_data(self, station_id: str, exclude: int = 0) -> Dict: return last_data - def check_not_updated(self, station_id: str, delay: int = 3600): + def check_not_updated(self, station_id: str, delay: int = 3600) -> List: """Check if a given station has not been updated.""" res = self.get_last_data(station_id) ret = [] @@ -202,7 +202,7 @@ def check_not_updated(self, station_id: str, delay: int = 3600): ret.append(key) return ret - def check_updated(self, station_id: str, delay: int = 3600): + def check_updated(self, station_id: str, delay: int = 3600) -> List: """Check if a given station has been updated.""" res = self.get_last_data(station_id) ret = [] @@ -222,7 +222,7 @@ def get_data( limit: int = None, optimize: bool = False, real_time: bool = False, - ): + ) -> Optional[Dict]: """Retrieve data from a device or module.""" post_params = {"device_id": device_id} if module_id: @@ -247,7 +247,7 @@ def get_data( def get_min_max_t_h( self, station_id: str, module_id: str = None, frame: str = "last24" - ): + ) -> Optional[Tuple[float, float, float, float]]: """Return minimum and maximum temperature and humidity over the given timeframe. Arguments: @@ -279,3 +279,5 @@ def get_min_max_t_h( temperature = [temp[0] for temp in resp["body"].values()] humidity = [hum[1] for hum in resp["body"].values()] return min(temperature), max(temperature), min(humidity), max(humidity) + + return None From 60e3b3b2946fe8d35562ba98556700ac5e83ae96 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 2 Jun 2020 23:59:02 +0200 Subject: [PATCH 250/333] Readd default values for id and secret in case token is given --- src/pyatmo/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 1a51fa992..bba44d729 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -42,8 +42,8 @@ class NetatmoOAuth2: def __init__( self, - client_id: str, - client_secret: str, + client_id: str = None, + client_secret: str = None, redirect_uri: Optional[str] = None, token: Optional[Dict[str, str]] = None, token_updater: Optional[Callable[[str], None]] = None, From b3e1f6aa93d5d7b7eb99b7b87edfa074b0946db3 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 3 Jun 2020 00:37:07 +0200 Subject: [PATCH 251/333] Remove default station --- src/pyatmo/weather_station.py | 9 +++------ tests/test_pyatmo_homecoach.py | 2 +- tests/test_pyatmo_weatherstation.py | 5 ++++- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index c6d74691d..56f92f775 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -60,13 +60,10 @@ def __init__(self, auth: NetatmoOAuth2, url_req: str = None) -> None: self.modules[module["_id"]] = module self.modules[module["_id"]]["main_device"] = item["_id"] - self.default_station = list(self.stations.values())[0]["station_name"] - def get_module_names(self, station_id: str) -> List: - """Return a list of all module names for a given or all stations.""" + """Return a list of all module names for a given station.""" res = set() station_data = self.get_station(station_id) - print(station_data) if not station_data: return [] @@ -79,7 +76,7 @@ def get_module_names(self, station_id: str) -> List: return list(res) def get_modules(self, station_id: str) -> Dict: - """Return a dict of modules for a given or all stations.""" + """Return a dict of modules for a given station.""" station_data = self.get_station(station_id) if not station_data: @@ -110,7 +107,7 @@ def get_module(self, module_id: str) -> Dict: return self.modules.get(module_id, {}) def get_monitored_conditions(self, module_id: str) -> List: - """Return monitored conditions for given module(s).""" + """Return monitored conditions for given module.""" module = self.get_module(module_id) if not module: module = self.get_station(module_id) diff --git a/tests/test_pyatmo_homecoach.py b/tests/test_pyatmo_homecoach.py index 05fd840c5..9179f207a 100644 --- a/tests/test_pyatmo_homecoach.py +++ b/tests/test_pyatmo_homecoach.py @@ -8,7 +8,7 @@ def test_home_coach_data(home_coach_data): - assert home_coach_data.default_station == "Bedroom" + assert home_coach_data.stations["12:34:56:26:69:0c"]["station_name"] == "Bedroom" @pytest.mark.parametrize( diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 875e256d4..69f7b88d7 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -9,7 +9,10 @@ def test_weather_station_data(weather_station_data): - assert weather_station_data.default_station == "MyStation" + assert ( + weather_station_data.stations["12:34:56:37:11:ca"]["station_name"] + == "MyStation" + ) def test_weather_station_data_no_response(auth, requests_mock): From 35e9dbea1d2673760a1385dd8e7ae44a9ddb9c7e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 3 Jun 2020 00:47:20 +0200 Subject: [PATCH 252/333] Fix comment --- src/pyatmo/weather_station.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 56f92f775..3acc989fc 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -76,7 +76,7 @@ def get_module_names(self, station_id: str) -> List: return list(res) def get_modules(self, station_id: str) -> Dict: - """Return a dict of modules for a given station.""" + """Return a dict of modules per given station.""" station_data = self.get_station(station_id) if not station_data: From a0ce5343ba2a70f223b2069ec7a4b54abdc1bf14 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 4 Jun 2020 00:24:14 +0200 Subject: [PATCH 253/333] Update gitignore --- .gitignore | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 37437b539..89a74b34f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,10 @@ pyatmo.egg-info/ *.pyc .DS_Store +archive/ +access.token +cov.xml + venv/ .venv @@ -214,10 +218,6 @@ dmypy.json ### Code ### .vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json # End of https://www.gitignore.io/api/code From cc978366bb18f9eb5ac14013e087447a8a0c8ab7 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 5 Jun 2020 21:03:02 +0200 Subject: [PATCH 254/333] handle no wifi_status property (#112) --- src/pyatmo/weather_station.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 1fab6a07c..36c112432 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -203,8 +203,8 @@ def lastData(self, station=None, exclude=0, byId=False): if key in s and ds["time_utc"] > limit: lastD[s[key]] = ds.copy() lastD[s[key]]["When"] = lastD[s[key]].pop("time_utc") - lastD[s[key]]["wifi_status"] = s["wifi_status"] - lastD[s[key]]["reachable"] = s["reachable"] + lastD[s[key]]["wifi_status"] = s.get("wifi_status") + lastD[s[key]]["reachable"] = s.get("reachable") for module in s["modules"]: if "dashboard_data" not in module or key not in module: continue From b63a7c7dc110d35bb254ad340016f0b657c7b53e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 6 Jun 2020 00:22:09 +0200 Subject: [PATCH 255/333] Minor improvements --- src/pyatmo/camera.py | 81 +++++++++++++++++++++----------------------- tests/conftest.py | 3 +- 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 2617d2d7a..75d2bdac8 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -77,7 +77,7 @@ def __init__(self, auth: NetatmoOAuth2, size: int = 30) -> None: if event["camera_id"] not in self.outdoor_events: self.outdoor_events[event["camera_id"]] = {} self.outdoor_events[event["camera_id"]][event["time"]] = event - elif event["type"] != "outdoor": + else: if event["camera_id"] not in self.events: self.events[event["camera_id"]] = {} self.events[event["camera_id"]][event["time"]] = event @@ -173,9 +173,8 @@ def persons_at_home(self, home_id: str = None) -> List: for person in home_data.get("persons"): # Only check known personshome - if "pseudo" in person: - if not person["out_of_sight"]: - at_home.append(person["pseudo"]) + if "pseudo" in person and not person["out_of_sight"]: + at_home.append(person["pseudo"]) return at_home def set_persons_home(self, person_ids: List[str], home_id: str): @@ -189,8 +188,7 @@ def set_persons_home(self, person_ids: List[str], home_id: str): "home_id": home_id, "person_ids[]": person_ids, } - resp = self.auth.post_request(url=_SETPERSONSHOME_REQ, params=post_params) - return resp + return self.auth.post_request(url=_SETPERSONSHOME_REQ, params=post_params) def set_persons_away(self, person_id: str, home_id: str): """Mark a person as away or set the whole home to being empty. @@ -203,8 +201,7 @@ def set_persons_away(self, person_id: str, home_id: str): "home_id": home_id, "person_id": person_id, } - resp = self.auth.post_request(url=_SETPERSONSAWAY_REQ, params=post_params) - return resp + return self.auth.post_request(url=_SETPERSONSAWAY_REQ, params=post_params) def get_person_id(self, name: str) -> Optional[str]: """Retrieve the ID of a person. @@ -235,11 +232,13 @@ def get_camera_picture( def get_profile_image(self, name: str) -> Tuple[Optional[bytes], Optional[str]]: """Retrieve the face of a given person.""" for person in self.persons: - if "pseudo" in self.persons[person]: - if name == self.persons[person]["pseudo"]: - image_id = self.persons[person]["face"]["id"] - key = self.persons[person]["face"]["key"] - return self.get_camera_picture(image_id, key) + if ( + "pseudo" in self.persons[person] + and name == self.persons[person]["pseudo"] + ): + image_id = self.persons[person]["face"]["id"] + key = self.persons[person]["face"]["key"] + return self.get_camera_picture(image_id, key) return None, None @@ -248,7 +247,7 @@ def update_events( ) -> None: """Update the list of events.""" # Either event_id or device_type must be given - if not event_id and not device_type: + if not (event_id or device_type): raise ApiError if device_type == "NACamera": @@ -263,7 +262,7 @@ def update_events( ] event_id = event_list[sorted(event_list)[0]]["id"] - if device_type == "NOC": + elif device_type == "NOC": # for the Presence camera if not event_id: # If no event is provided we need to retrieve the oldest of @@ -275,7 +274,7 @@ def update_events( ] = self.outdoor_last_event[cam_id] event_id = event_list[sorted(event_list)[0]]["id"] - if device_type == "NSD": + elif device_type == "NSD": # for the smoke detector if not event_id: # If no event is provided we need to retrieve the oldest of @@ -306,7 +305,7 @@ def update_events( if event["camera_id"] not in self.outdoor_events: self.outdoor_events[event["camera_id"]] = {} self.outdoor_events[event["camera_id"]][event["time"]] = event - elif event["type"] != "outdoor": + else: if event["camera_id"] not in self.events: self.events[event["camera_id"]] = {} self.events[event["camera_id"]][event["time"]] = event @@ -334,15 +333,19 @@ def person_seen_by_camera( return False if self.events[camera_id][time_ev]["type"] == "person": person_id = self.events[camera_id][time_ev]["person_id"] - if "pseudo" in self.persons[person_id]: - if self.persons[person_id]["pseudo"] == name: - return True + if ( + "pseudo" in self.persons[person_id] + and self.persons[person_id]["pseudo"] == name + ): + return True elif self.last_event[camera_id]["type"] == "person": person_id = self.last_event[camera_id]["person_id"] - if "pseudo" in self.persons[person_id]: - if self.persons[person_id]["pseudo"] == name: - return True + if ( + "pseudo" in self.persons[person_id] + and self.persons[person_id]["pseudo"] == name + ): + return True return False def _known_persons(self) -> Dict[str, Dict]: @@ -356,10 +359,7 @@ def known_persons(self) -> Dict[str, str]: return {pid: p["pseudo"] for pid, p in self._known_persons().items()} def known_persons_names(self) -> List[str]: - names = [] - for person in self._known_persons().values(): - names.append(person["pseudo"]) - return names + return [person["pseudo"] for person in self._known_persons().values()] def someone_known_seen(self, camera_id: str, exclude: int = 0) -> bool: """Evaluate if someone known has been seen.""" @@ -423,15 +423,12 @@ def motion_detected(self, camera_id: str, exclude: int = 0) -> bool: def outdoor_motion_detected(self, camera_id: str, offset: int = 0) -> bool: """Evaluate if outdoor movement has been detected.""" - if camera_id in self.last_event: - if self.last_event[camera_id]["type"] == "movement": - if self.last_event[camera_id][ - "video_status" - ] == "recording" and self.last_event[camera_id]["time"] + offset > int( - time.time() - ): - return True - return False + return ( + camera_id in self.last_event + and self.last_event[camera_id]["type"] == "movement" + and self.last_event[camera_id]["video_status"] == "recording" + and self.last_event[camera_id]["time"] + offset > int(time.time()) + ) def human_detected(self, camera_id: str, offset: int = 0) -> bool: """Evaluate if a human has been detected.""" @@ -474,16 +471,14 @@ def module_motion_detected( if time_ev < limit: return False if ( - self.events[camera_id][time_ev]["type"] == "tag_big_move" - or self.events[camera_id][time_ev]["type"] == "tag_small_move" - ) and self.events[camera_id][time_ev]["module_id"] == module_id: + self.events[camera_id][time_ev]["type"] + in ["tag_big_move", "tag_small_move"] + and self.events[camera_id][time_ev]["module_id"] == module_id + ): return True elif ( camera_id in self.last_event - and ( - self.last_event[camera_id]["type"] == "tag_big_move" - or self.last_event[camera_id]["type"] == "tag_small_move" - ) + and self.last_event[camera_id]["type"] in ["tag_big_move", "tag_small_move"] and self.last_event[camera_id]["module_id"] == module_id ): return True diff --git a/tests/conftest.py b/tests/conftest.py index ab7141506..2258385f9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,14 +22,13 @@ def auth(requests_mock): json=json_fixture, headers={"content-type": "application/json"}, ) - authorization = pyatmo.ClientAuth( + return pyatmo.ClientAuth( client_id="CLIENT_ID", client_secret="CLIENT_SECRET", username="USERNAME", password="PASSWORD", scope=" ".join(pyatmo.auth.ALL_SCOPES), ) - return authorization @pytest.fixture(scope="function") From 0be03f50a6d69b5e6bad299307563cee49dffe15 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 8 Jun 2020 16:17:42 +0200 Subject: [PATCH 256/333] Make error more explicit --- src/pyatmo/camera.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 75d2bdac8..0bb39d28a 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -146,9 +146,12 @@ def update_camera_urls(self, camera_id: str) -> None: def check_url(url: str) -> Optional[str]: try: resp = self.auth.post_request(url=f"{url}/command/ping") - except (ApiError, ReadTimeout): + except ReadTimeout: LOG.debug("Timeout validation of camera url %s", url) return None + except ApiError: + LOG.debug("Api error for camera url %s", url) + return None else: return resp.get("local_url") From bcc2de58f8b48b1f62a03d0741710eb026d114fa Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 9 Jun 2020 11:18:51 +0200 Subject: [PATCH 257/333] Improve test for event update --- fixtures/camera_data_events_until.json | 60 ++++++++++++++++++++++++-- tests/test_pyatmo_camera.py | 8 +++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/fixtures/camera_data_events_until.json b/fixtures/camera_data_events_until.json index 4d7d3efad..941d3c1f9 100644 --- a/fixtures/camera_data_events_until.json +++ b/fixtures/camera_data_events_until.json @@ -4,7 +4,7 @@ { "id": "a1b2c3d4e5f6abcdef123461", "type": "person", - "time": 1560506232, + "time": 1560706232, "camera_id": "12:34:56:00:f1:62", "device_id": "12:34:56:00:f1:62", "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", @@ -15,7 +15,7 @@ { "id": "a1b2c3d4e5f6abcdef123462", "type": "person_away", - "time": 1560506237, + "time": 1560706237, "camera_id": "12:34:56:00:f1:62", "device_id": "12:34:56:00:f1:62", "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", @@ -25,7 +25,7 @@ { "id": "a1b2c3d4e5f6abcdef123463", "type": "person", - "time": 1560506241, + "time": 1560706241, "camera_id": "12:34:56:00:f1:62", "device_id": "12:34:56:00:f1:62", "person_id": "91827375-7e04-5298-83ae-a0cb8372dff2", @@ -39,10 +39,62 @@ "video_status": "available", "is_arrival": false, "message": "Jane Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef123464", + "type": "wifi_status", + "time": 1560706271, + "camera_id": "12:34:56:00:8b:a2", + "device_id": "12:34:56:00:8b:a2", + "sub_type": 1, + "message": "Hall:WLAN-Verbindung erfolgreich hergestellt" + }, + { + "id": "a1b2c3d4e5f6abcdef123465", + "type": "outdoor", + "time": 1560706283, + "camera_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:00:a5:a4", + "video_id": "string", + "video_status": "available", + "event_list": [ + { + "type": "string", + "time": 1560706283, + "offset": 0, + "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0001", + "message": "Animal dÊtectÊ", + "snapshot": { + "id": "5715e16849c75xxxx00000000xxxxx", + "version": 1, + "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000", + "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa" + }, + "vignette": { + "id": "5715e16849c75xxxx00000000xxxxx", + "version": 1, + "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000", + "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa00000" + } + }, + { + "type": "string", + "time": 1560706283, + "offset": 0, + "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0002", + "message": "Animal dÊtectÊ", + "snapshot": { + "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c53b-aze7a.jpg" + }, + "vignette": { + "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c5.jpg" + } + } + ] } ] }, "status": "ok", "time_exec": 0.03666909215079, - "time_server": 15605062321 + "time_server": 15607062321 } \ No newline at end of file diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 02d1d96c5..375c66bb9 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -159,6 +159,8 @@ def test_camera_data_person_seen_by_camera( def test_camera_data__known_persons(camera_home_data): known_persons = camera_home_data._known_persons() + print(known_persons) + print(known_persons.keys()) assert len(known_persons) == 3 assert known_persons["91827374-7e04-5298-83ad-a0cb8372dff1"]["pseudo"] == "John Doe" @@ -434,7 +436,7 @@ def test_camera_data_get_profile_image(camera_home_data, requests_mock): ("91763b24c43d3e344f424e8b", None, "NSD", does_not_raise()), ], ) -def test_camera_data_update_event( +def test_camera_data_update_events( camera_home_data, requests_mock, home_id, event_id, device_type, exception ): with open("fixtures/camera_data_events_until.json") as fixture_file: @@ -445,12 +447,16 @@ def test_camera_data_update_event( headers={"content-type": "application/json"}, ) with exception: + before_outdoor = camera_home_data.outdoor_last_event.copy() + before = camera_home_data.last_event.copy() assert ( camera_home_data.update_events( home_id=home_id, event_id=event_id, device_type=device_type ) is None ) + assert camera_home_data.outdoor_last_event != before_outdoor + assert camera_home_data.last_event != before def test_camera_data_outdoor_motion_detected(camera_home_data): From ff7b57341030579c5741728d996dc0a23c498efe Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 9 Jun 2020 12:28:40 +0200 Subject: [PATCH 258/333] Some formatting improvements --- src/pyatmo/camera.py | 105 +++++++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 34 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 0bb39d28a..f5723f551 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -60,38 +60,50 @@ def __init__(self, auth: NetatmoOAuth2, size: int = 30) -> None: for item in self.raw_data: home_id: str = item.get("id") home_name: str = item.get("name") + if not home_name: home_name = "Unknown" self.homes[home_id]["name"] = home_name + if home_id not in self.cameras: self.cameras[home_id] = {} + if home_id not in self.smokedetectors: self.smokedetectors[home_id] = {} + if home_id not in self.types: self.types[home_id] = {} + for person in item["persons"]: self.persons[person["id"]] = person + if "events" in item: for event in item["events"]: if event["type"] == "outdoor": if event["camera_id"] not in self.outdoor_events: self.outdoor_events[event["camera_id"]] = {} self.outdoor_events[event["camera_id"]][event["time"]] = event + else: if event["camera_id"] not in self.events: self.events[event["camera_id"]] = {} self.events[event["camera_id"]][event["time"]] = event + for camera in item["cameras"]: self.cameras[home_id][camera["id"]] = camera self.cameras[home_id][camera["id"]]["home_id"] = home_id + if camera["type"] == "NACamera" and "modules" in camera: for module in camera["modules"]: self.modules[module["id"]] = module self.modules[module["id"]]["cam_id"] = camera["id"] + for smoke in item["smokedetectors"]: self.smokedetectors[home_id][smoke["id"]] = smoke + for camera_type in item["cameras"]: self.types[home_id][camera_type["type"]] = camera_type + for smoke_type in item["smokedetectors"]: self.types[home_id][smoke_type["type"]] = smoke_type @@ -99,6 +111,7 @@ def __init__(self, auth: NetatmoOAuth2, size: int = 30) -> None: self.last_event[camera] = self.events[camera][ sorted(self.events[camera])[-1] ] + for camera in self.outdoor_events: self.outdoor_last_event[camera] = self.outdoor_events[camera][ sorted(self.outdoor_events[camera])[-1] @@ -113,6 +126,7 @@ def get_camera(self, camera_id: str) -> Dict[str, str]: for home_id, _ in self.cameras.items(): if camera_id in self.cameras[home_id]: return self.cameras[home_id][camera_id] + return {} def get_module(self, module_id: str) -> Optional[dict]: @@ -124,6 +138,7 @@ def get_smokedetector(self, smoke_id: str) -> Optional[dict]: for home_id, _ in self.smokedetectors.items(): if smoke_id in self.smokedetectors[home_id]: return self.smokedetectors[home_id][smoke_id] + return None def camera_urls(self, camera_id: str) -> Tuple[Optional[str], Optional[str]]: @@ -172,13 +187,11 @@ def get_light_state(self, camera_id: str) -> Optional[str]: def persons_at_home(self, home_id: str = None) -> List: """Return a list of known persons who are currently at home.""" home_data = self.homes.get(home_id, {}) - at_home = [] - - for person in home_data.get("persons"): - # Only check known personshome - if "pseudo" in person and not person["out_of_sight"]: - at_home.append(person["pseudo"]) - return at_home + return [ + person["pseudo"] + for person in home_data.get("persons") + if "pseudo" in person and not person["out_of_sight"] + ] def set_persons_home(self, person_ids: List[str], home_id: str): """Mark persons as home. @@ -218,6 +231,7 @@ def get_person_id(self, name: str) -> Optional[str]: for pid, data in self.persons.items(): if "pseudo" in data and name == data["pseudo"]: return pid + return None def get_camera_picture( @@ -253,41 +267,30 @@ def update_events( if not (event_id or device_type): raise ApiError + def get_event_id(data: Dict): + events = {e["time"]: e for e in data.values()} + return min(events.items())[1].get("id") + if device_type == "NACamera": # for the Welcome camera if not event_id: # If no event is provided we need to retrieve the oldest of # the last event seen by each camera - event_list = {} - for cam_id in self.last_event: - event_list[self.last_event[cam_id]["time"]] = self.last_event[ - cam_id - ] - event_id = event_list[sorted(event_list)[0]]["id"] + event_id = get_event_id(self.last_event) elif device_type == "NOC": # for the Presence camera if not event_id: # If no event is provided we need to retrieve the oldest of # the last event seen by each camera - event_list = {} - for cam_id in self.outdoor_last_event: - event_list[ - self.outdoor_last_event[cam_id]["time"] - ] = self.outdoor_last_event[cam_id] - event_id = event_list[sorted(event_list)[0]]["id"] + event_id = get_event_id(self.outdoor_last_event) elif device_type == "NSD": # for the smoke detector if not event_id: # If no event is provided we need to retrieve the oldest of # the last event by each smoke detector - event_list = {} - for smoke_id in self.outdoor_last_event: - event_list[ - self.outdoor_last_event[smoke_id]["time"] - ] = self.outdoor_last_event[smoke_id] - event_id = event_list[sorted(event_list)[0]]["id"] + event_id = get_event_id(self.outdoor_last_event) post_params = { "home_id": home_id, @@ -308,14 +311,17 @@ def update_events( if event["camera_id"] not in self.outdoor_events: self.outdoor_events[event["camera_id"]] = {} self.outdoor_events[event["camera_id"]][event["time"]] = event + else: if event["camera_id"] not in self.events: self.events[event["camera_id"]] = {} self.events[event["camera_id"]][event["time"]] = event + for camera in self.events: self.last_event[camera] = self.events[camera][ sorted(self.events[camera])[-1] ] + for camera in self.outdoor_events: self.outdoor_last_event[camera] = self.outdoor_events[camera][ sorted(self.outdoor_events[camera])[-1] @@ -324,18 +330,19 @@ def update_events( def person_seen_by_camera( self, name: str, camera_id: str, exclude: int = 0 ) -> bool: - """ - Evaluate if a specific person has been seen - """ + """Evaluate if a specific person has been seen.""" # Check in the last event is someone known has been seen if exclude: limit = time.time() - exclude array_time_event = sorted(self.events[camera_id], reverse=True) + for time_ev in array_time_event: if time_ev < limit: return False + if self.events[camera_id][time_ev]["type"] == "person": person_id = self.events[camera_id][time_ev]["person_id"] + if ( "pseudo" in self.persons[person_id] and self.persons[person_id]["pseudo"] == name @@ -344,84 +351,103 @@ def person_seen_by_camera( elif self.last_event[camera_id]["type"] == "person": person_id = self.last_event[camera_id]["person_id"] + if ( "pseudo" in self.persons[person_id] and self.persons[person_id]["pseudo"] == name ): return True + return False def _known_persons(self) -> Dict[str, Dict]: - known_persons = {} - for person_id, person in self.persons.items(): - if "pseudo" in person: - known_persons[person_id] = person - return known_persons + """Return all known persons.""" + return {pid: p for pid, p in self.persons.items() if "pseudo" in p} def known_persons(self) -> Dict[str, str]: + """Return a dictionary of known person names.""" return {pid: p["pseudo"] for pid, p in self._known_persons().items()} def known_persons_names(self) -> List[str]: + """Return a list of known person names.""" return [person["pseudo"] for person in self._known_persons().values()] def someone_known_seen(self, camera_id: str, exclude: int = 0) -> bool: """Evaluate if someone known has been seen.""" if camera_id not in self.events: raise NoDevice + if exclude: limit = time.time() - exclude array_time_event = sorted(self.events[camera_id], reverse=True) + for time_ev in array_time_event: if time_ev < limit: return False + if self.events[camera_id][time_ev]["type"] == "person": if ( self.events[camera_id][time_ev]["person_id"] in self._known_persons() ): return True + # Check in the last event if someone known has been seen elif self.last_event[camera_id]["type"] == "person": + if self.last_event[camera_id]["person_id"] in self._known_persons(): return True + return False def someone_unknown_seen(self, camera_id: str, exclude: int = 0) -> bool: """Evaluate if someone known has been seen.""" if camera_id not in self.events: raise NoDevice + if exclude: limit = time.time() - exclude array_time_event = sorted(self.events[camera_id], reverse=True) + for time_ev in array_time_event: if time_ev < limit: return False + if self.events[camera_id][time_ev]["type"] == "person": + if ( self.events[camera_id][time_ev]["person_id"] not in self._known_persons() ): return True - # Check in the last event is someone known has been seen + + # Check in the last event is noone known has been seen elif self.last_event[camera_id]["type"] == "person": + if self.last_event[camera_id]["person_id"] not in self._known_persons(): return True + return False def motion_detected(self, camera_id: str, exclude: int = 0) -> bool: """Evaluate if movement has been detected.""" if camera_id not in self.events: raise NoDevice + if exclude: limit = time.time() - exclude array_time_event = sorted(self.events[camera_id], reverse=True) + for time_ev in array_time_event: if time_ev < limit: return False + if self.events[camera_id][time_ev]["type"] == "movement": return True + elif self.last_event[camera_id]["type"] == "movement": return True + return False def outdoor_motion_detected(self, camera_id: str, offset: int = 0) -> bool: @@ -441,6 +467,7 @@ def human_detected(self, camera_id: str, offset: int = 0) -> bool: time.time() ): return True + return False def animal_detected(self, camera_id: str, offset: int = 0) -> bool: @@ -451,6 +478,7 @@ def animal_detected(self, camera_id: str, offset: int = 0) -> bool: time.time() ): return True + return False def car_detected(self, camera_id: str, offset: int = 0) -> bool: @@ -461,6 +489,7 @@ def car_detected(self, camera_id: str, offset: int = 0) -> bool: time.time() ): return True + return False def module_motion_detected( @@ -470,21 +499,25 @@ def module_motion_detected( if exclude: limit = time.time() - exclude array_time_event = sorted(self.events.get(camera_id, []), reverse=True) + for time_ev in array_time_event: if time_ev < limit: return False + if ( self.events[camera_id][time_ev]["type"] in ["tag_big_move", "tag_small_move"] and self.events[camera_id][time_ev]["module_id"] == module_id ): return True + elif ( camera_id in self.last_event and self.last_event[camera_id]["type"] in ["tag_big_move", "tag_small_move"] and self.last_event[camera_id]["module_id"] == module_id ): return True + return False def module_opened(self, module_id: str, camera_id: str, exclude: int = 0) -> bool: @@ -492,19 +525,23 @@ def module_opened(self, module_id: str, camera_id: str, exclude: int = 0) -> boo if exclude: limit = time.time() - exclude array_time_event = sorted(self.events.get(camera_id, []), reverse=True) + for time_ev in array_time_event: if time_ev < limit: return False + if ( self.events[camera_id][time_ev]["type"] == "tag_open" and self.events[camera_id][time_ev]["module_id"] == module_id ): return True + elif camera_id in self.last_event and ( self.last_event[camera_id]["type"] == "tag_open" and self.last_event[camera_id]["module_id"] == module_id ): return True + return False def set_state( From cfe779de5bc0a6877225c6ef48a7e2dfa09f9056 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 9 Jun 2020 12:32:39 +0200 Subject: [PATCH 259/333] Formatting --- src/pyatmo/weather_station.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 3acc989fc..e86cf738e 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -13,9 +13,7 @@ class WeatherStationData: - """ - Class of Netatmo Weather Station devices (stations and modules) - """ + """Class of Netatmo Weather Station devices (stations and modules).""" def __init__(self, auth: NetatmoOAuth2, url_req: str = None) -> None: """Initialize self. @@ -122,18 +120,24 @@ def get_monitored_conditions(self, module_id: str) -> List: conditions.extend( ["WindAngle", "WindStrength", "GustAngle", "GustStrength"] ) + elif condition == "Rain": conditions.extend(["Rain", "sum_rain_24", "sum_rain_1"]) + else: conditions.append(condition) + if module["type"] in ["NAMain", "NHC"]: # the main module has wifi_status conditions.append("wifi_status") + else: # assume all other modules have rf_status, battery_vp, and battery_percent conditions.extend(["rf_status", "battery_vp", "battery_percent"]) + if module["type"] in ["NAMain", "NAModule1", "NAModule4", "NHC"]: conditions.extend(["min_temp", "max_temp"]) + if module["type"] in [ "NAMain", "NAModule1", @@ -143,6 +147,7 @@ def get_monitored_conditions(self, module_id: str) -> List: "NHC", ]: conditions.append("reachable") + return conditions def get_last_data(self, station_id: str, exclude: int = 0) -> Dict: @@ -197,6 +202,7 @@ def check_not_updated(self, station_id: str, delay: int = 3600) -> List: for key, value in res.items(): if time.time() - value["When"] > delay: ret.append(key) + return ret def check_updated(self, station_id: str, delay: int = 3600) -> List: @@ -206,6 +212,7 @@ def check_updated(self, station_id: str, delay: int = 3600) -> List: for key, value in res.items(): if time.time() - value["When"] < delay: ret.append(key) + return ret def get_data( @@ -260,6 +267,7 @@ def get_min_max_t_h( if frame == "last24": end = time.time() start = end - 24 * 3600 # 24 hours ago + elif frame == "day": start, end = today_stamps() From c9da481b5b9a5dbd7f6e7592051a2e350262a9bb Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 9 Jun 2020 12:34:23 +0200 Subject: [PATCH 260/333] More formatting --- src/pyatmo/thermostat.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 477853534..3af9b9ba8 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -49,36 +49,48 @@ def __init__(self, auth: NetatmoOAuth2) -> None: for item in self.raw_data: home_id = item.get("id") home_name = item.get("name") + if not home_name: home_name = "Unknown" self.homes[home_id]["name"] = home_name + if "modules" in item: if home_id not in self.modules: self.modules[home_id] = {} + for module in item["modules"]: self.modules[home_id][module["id"]] = module + if home_id not in self.rooms: self.rooms[home_id] = {} + if home_id not in self.schedules: self.schedules[home_id] = {} + if home_id not in self.zones: self.zones[home_id] = {} + if home_id not in self.setpoint_duration: self.setpoint_duration[home_id] = {} + if "therm_setpoint_default_duration" in item: self.setpoint_duration[home_id] = item[ "therm_setpoint_default_duration" ] + if "rooms" in item: for room in item["rooms"]: self.rooms[home_id][room["id"]] = room + if "therm_schedules" in item: for schedule in item["therm_schedules"]: self.schedules[home_id][schedule["id"]] = schedule + for schedule in item["therm_schedules"]: schedule_id = schedule["id"] if schedule_id not in self.zones[home_id]: self.zones[home_id][schedule_id] = {} + for zone in schedule["zones"]: self.zones[home_id][schedule_id][zone["id"]] = zone @@ -87,6 +99,7 @@ def _get_selected_schedule(self, home_id: str) -> Dict: for value in self.schedules.get(home_id, {}).values(): if "selected" in value.keys(): return value + return {} def switch_home_schedule(self, home_id: str, schedule_id: str) -> Any: @@ -118,6 +131,7 @@ def get_thermostat_type(self, home_id: str, room_id: str) -> Optional[str]: for module in self.modules.get(home_id, {}).values(): if module.get("room_id") == room_id: return module.get("type") + return None @@ -152,22 +166,28 @@ def __init__(self, auth: NetatmoOAuth2, home_id: str): thermostat_id = module["id"] if thermostat_id not in self.thermostats: self.thermostats[thermostat_id] = {} + self.thermostats[thermostat_id] = module + elif module["type"] == "NRV": valve_id = module["id"] if valve_id not in self.valves: self.valves[valve_id] = {} + self.valves[valve_id] = module + elif module["type"] == "NAPlug": relay_id = module["id"] if relay_id not in self.relays: self.relays[relay_id] = {} + self.relays[relay_id] = module def get_room(self, room_id: str) -> Dict: for key, value in self.rooms.items(): if value["id"] == room_id: return self.rooms[key] + raise InvalidRoom("No room with ID %s" % room_id) def get_thermostat(self, room_id: str) -> Dict: @@ -175,18 +195,21 @@ def get_thermostat(self, room_id: str) -> Dict: for key, value in self.thermostats.items(): if value["id"] == room_id: return self.thermostats[key] + raise InvalidRoom("No room with ID %s" % room_id) def get_relay(self, room_id: str) -> Dict: for key, value in self.relays.items(): if value["id"] == room_id: return self.relays[key] + raise InvalidRoom("No room with ID %s" % room_id) def get_valve(self, room_id: str) -> Dict: for key, value in self.valves.items(): if value["id"] == room_id: return self.valves[key] + raise InvalidRoom("No room with ID %s" % room_id) def set_point(self, room_id: str) -> Optional[float]: @@ -213,8 +236,10 @@ def set_thermmode( } if end_time is not None and mode in ("hg", "away"): post_params["endtime"] = str(end_time) + if schedule_id is not None and mode == "schedule": post_params["schedule_id"] = schedule_id + return self.auth.post_request(url=_SETTHERMMODE_REQ, params=post_params) def set_room_thermpoint( @@ -229,6 +254,8 @@ def set_room_thermpoint( # handle that even when mode == 'home' and these settings don't make sense if temp is not None: post_params["temp"] = str(temp) + if end_time is not None: post_params["endtime"] = str(end_time) + return self.auth.post_request(url=_SETROOMTHERMPOINT_REQ, params=post_params) From 59577ae5644c3c8b2658f676dd51eb27a35e59ec Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 9 Jun 2020 12:39:34 +0200 Subject: [PATCH 261/333] And some more --- src/pyatmo/public_data.py | 11 ++++++----- src/pyatmo/weather_station.py | 3 +++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pyatmo/public_data.py b/src/pyatmo/public_data.py index 6c96d21ae..a044ecc3e 100644 --- a/src/pyatmo/public_data.py +++ b/src/pyatmo/public_data.py @@ -33,7 +33,7 @@ def __init__( LON_NE: str, LAT_SW: str, LON_SW: str, - required_data_type: str = None, # comma-separated list from above _STATION or _ACCESSORY values + required_data_type: str = None, filtering: bool = False, ) -> None: """Initialize self. @@ -46,7 +46,7 @@ def __init__( LON_SW {str} -- Longitude of the south west corner of the requested area. (-180 <= LON_SW <= 180) Keyword Arguments: - required_data_type {str} -- [description] (default: {None}) + required_data_type {str} -- comma-separated list from above _STATION or _ACCESSORY values (default: {None}) Raises: NoDevice: No devices found. @@ -134,6 +134,7 @@ def get_locations(self) -> Dict: locations: Dict = {} for station in self.raw_data: locations[station["_id"]] = station["place"]["location"] + return locations def get_time_for_rain_measures(self) -> Dict: @@ -157,6 +158,7 @@ def get_latest_station_measures(self, data_type) -> Dict: measures[station["_id"]] = module["res"][latest_timestamp][ measure_index ] + return measures def get_accessory_data(self, data_type: str) -> Dict[str, Any]: @@ -165,10 +167,9 @@ def get_accessory_data(self, data_type: str) -> Dict[str, Any]: for module in station["measures"].values(): if data_type in module: data[station["_id"]] = module[data_type] + return data def average(data: dict) -> float: - if data: - return sum(data.values()) / len(data) - return 0.0 + return sum(data.values()) / len(data) if data else 0.0 diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index e86cf738e..831be95a5 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -31,6 +31,7 @@ def __init__(self, auth: NetatmoOAuth2, url_req: str = None) -> None: if resp is None or "body" not in resp: raise NoDevice("No weather station data returned by Netatmo server") + try: self.raw_data = fix_id(resp["body"].get("devices")) except KeyError: @@ -52,6 +53,7 @@ def __init__(self, auth: NetatmoOAuth2, url_req: str = None) -> None: if "module_name" not in module: if module["type"] == "NHC": module["module_name"] = module["station_name"] + else: continue @@ -94,6 +96,7 @@ def get_modules(self, station_id: str) -> Dict: "module_name": module.get("module_name", module.get("type")), "id": module["_id"], } + return res def get_station(self, station_id: str) -> Dict: From 1228f9b374336045ed2513eb7def7cd513c84a3b Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 9 Jun 2020 12:40:48 +0200 Subject: [PATCH 262/333] And auth too --- src/pyatmo/auth.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index bba44d729..af72c138e 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -74,8 +74,10 @@ def __init__( self.client_secret = client_secret self.redirect_uri = redirect_uri self.token_updater = token_updater + if token: self.scope = " ".join(token["scope"]) + else: self.scope = " ".join(ALL_SCOPES) if not scope else scope @@ -108,6 +110,7 @@ def post_request( if "json" in params: json_params: Optional[str] = params.pop("json") + else: json_params = None @@ -120,21 +123,25 @@ def post_request( LOG.debug("Connection to %s timed out", url) except requests.exceptions.ConnectionError: LOG.debug("Remote end closed connection without response (%s)", url) + else: def query(url: str, params: Dict, timeout: int, retries: int) -> Any: if retries == 0: LOG.error("Too many retries") return + try: if json_params: rsp = self._oauth.post( url=url, json=json_params, timeout=timeout ) + else: rsp = self._oauth.post(url=url, data=params, timeout=timeout) return rsp + except ( TokenExpiredError, requests.exceptions.ReadTimeout, @@ -163,6 +170,7 @@ def query(url: str, params: Dict, timeout: int, retries: int) -> Any: f"({resp.json()['error']['code']}) " f"when accessing '{url}'" ) + except JSONDecodeError: raise ApiError( f"{resp.status_code} - " @@ -173,8 +181,10 @@ def query(url: str, params: Dict, timeout: int, retries: int) -> Any: try: if "application/json" in resp.headers.get("content-type", []): return resp.json() + if resp.content not in [b"", b"None"]: return resp.content + except (TypeError, AttributeError): LOG.debug("Invalid response %s", resp) From 37946ce06b1f63aff167aa652d05a473dd050a66 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 14 Jun 2020 01:14:50 +0200 Subject: [PATCH 263/333] Fix camera url update issue --- src/pyatmo/camera.py | 46 ++++++++++++++++++++----------------- tests/test_pyatmo_camera.py | 23 ++----------------- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index f5723f551..290b6292c 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -154,27 +154,31 @@ def update_camera_urls(self, camera_id: str) -> None: camera_data = self.get_camera(camera_id) home_id = camera_data["home_id"] - if camera_data: - vpn_url = camera_data.get("vpn_url") - if vpn_url and camera_data.get("is_local"): - - def check_url(url: str) -> Optional[str]: - try: - resp = self.auth.post_request(url=f"{url}/command/ping") - except ReadTimeout: - LOG.debug("Timeout validation of camera url %s", url) - return None - except ApiError: - LOG.debug("Api error for camera url %s", url) - return None - else: - return resp.get("local_url") - - temp_local_url = check_url(vpn_url) - if temp_local_url: - self.cameras[home_id][camera_id]["local_url"] = check_url( - temp_local_url - ) + if not camera_data or camera_data.get("status") == "disconnected": + self.cameras[home_id][camera_id]["local_url"] = None + self.cameras[home_id][camera_id]["vpn_url"] = None + return + + vpn_url = camera_data.get("vpn_url") + if vpn_url and camera_data.get("is_local"): + + def check_url(url: str) -> Optional[str]: + try: + resp = self.auth.post_request(url=f"{url}/command/ping") + except ReadTimeout: + LOG.debug("Timeout validation of camera url %s", url) + return None + except ApiError: + LOG.debug("Api error for camera url %s", url) + return None + else: + return resp.get("local_url") + + temp_local_url = check_url(vpn_url) + if temp_local_url: + self.cameras[home_id][camera_id]["local_url"] = check_url( + temp_local_url + ) def get_light_state(self, camera_id: str) -> Optional[str]: """Return the current mode of the floodlight of a presence camera.""" diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 375c66bb9..6a232ae6e 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -104,29 +104,10 @@ def test_camera_data_camera_urls_disconnected(auth, requests_mock): headers={"content-type": "application/json"}, ) camera_data = pyatmo.CameraData(auth) - cid = "12:34:56:00:f1:62" - vpn_url = ( - "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/" - "6d278460699e56180d47ab47169efb31/" - "MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTg,," - ) - local_url = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" - with open("fixtures/camera_ping.json") as fixture_file: - json_fixture = json.load(fixture_file) - requests_mock.post( - vpn_url + "/command/ping", - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with open("fixtures/camera_ping.json") as fixture_file: - json_fixture = json.load(fixture_file) - requests_mock.post( - local_url + "/command/ping", - json=json_fixture, - headers={"content-type": "application/json"}, - ) + camera_data.update_camera_urls(cid) + assert camera_data.camera_urls(cid) == (None, None) From b6da77a907d358db36f2df8444f4b663842e7053 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Sat, 20 Jun 2020 22:04:01 +0200 Subject: [PATCH 264/333] Upgrade deps Signed-off-by: Hugo D. (jabesq) --- Pipfile.lock | 224 +++++++++++++++++++++++++-------------------------- tox.ini | 2 +- 2 files changed, 113 insertions(+), 113 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index ff9ba17aa..eed73cb4e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,10 +18,10 @@ "default": { "certifi": { "hashes": [ - "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", - "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" ], - "version": "==2020.4.5.1" + "version": "==2020.6.20" }, "chardet": { "hashes": [ @@ -50,11 +50,11 @@ }, "requests": { "hashes": [ - "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", - "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], "index": "pypi", - "version": "==2.23.0" + "version": "==2.24.0" }, "requests-oauthlib": { "hashes": [ @@ -75,17 +75,17 @@ "develop": { "appdirs": { "hashes": [ - "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", - "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" ], - "version": "==1.4.3" + "version": "==1.4.4" }, "astroid": { "hashes": [ - "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", - "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" + "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", + "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], - "version": "==2.4.1" + "version": "==2.4.2" }, "attrs": { "hashes": [ @@ -112,10 +112,10 @@ }, "certifi": { "hashes": [ - "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", - "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" ], - "version": "==2020.4.5.1" + "version": "==2020.6.20" }, "cfgv": { "hashes": [ @@ -197,11 +197,11 @@ }, "flake8": { "hashes": [ - "sha256:c09e7e4ea0d91fa36f7b8439ca158e592be56524f0b67c39ab0ea2b85ed8f9a4", - "sha256:f33c5320eaa459cdee6367016a4bf4ba2a9b81499ce56e6a32abbf0b8d3a2eb4" + "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", + "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" ], "index": "pypi", - "version": "==3.8.0a2" + "version": "==3.8.3" }, "freezegun": { "hashes": [ @@ -213,10 +213,10 @@ }, "identify": { "hashes": [ - "sha256:23c18d97bb50e05be1a54917ee45cc61d57cb96aedc06aabb2b02331edf0dbf0", - "sha256:88ed90632023e52a6495749c6732e61e08ec9f4f04e95484a5c37b9caf40283c" + "sha256:249ebc7e2066d6393d27c1b1be3b70433f824a120b1d8274d362f1eb419e3b52", + "sha256:781fd3401f5d2b17b22a8b18b493a48d5d948e3330634e82742e23f9c20234ef" ], - "version": "==1.4.15" + "version": "==1.4.19" }, "idna": { "hashes": [ @@ -227,11 +227,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", - "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" + "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545", + "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958" ], "markers": "python_version < '3.8'", - "version": "==1.6.0" + "version": "==1.6.1" }, "isort": { "hashes": [ @@ -283,30 +283,30 @@ }, "more-itertools": { "hashes": [ - "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", - "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" + "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5", + "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2" ], - "version": "==8.2.0" + "version": "==8.4.0" }, "mypy": { "hashes": [ - "sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2", - "sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1", - "sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164", - "sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761", - "sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce", - "sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27", - "sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754", - "sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae", - "sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9", - "sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600", - "sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65", - "sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8", - "sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913", - "sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3" + "sha256:1fe322a51df7ec60e4060c358422732359dda4467c1bd240f2172433b26b39be", + "sha256:2bc1fe8f793f1b1809afd6f57c2d9b948e1bf525a78e4c2957392a383c86a4a4", + "sha256:43335f3ff3288eb877de6d186ec08e10ea61093405189678756e14c7ccee4a9b", + "sha256:5c75603ea44bffad0356df333bd1b411facad321e0692d6b0687d65d94fcd8e1", + "sha256:738a8df84da4e9ce1f59cbdebc7d1d03310149405df9a95308d4f2a62a6d7c13", + "sha256:845abd8a9537da01e6b96f091e2d6d4ece18e52339f81f9fbbac1b6db2aa8d2d", + "sha256:94bb664868b5cf4ca1147d875a4c77883d8c605cf2e916853006e4c6194f1e84", + "sha256:b0c6ae0cb991a7a11013d0cc7edf8343184465b6e2dcbb9e44265c7bb3fde3af", + "sha256:ca56382e6ebdeb3cb928ae4ce87031cc752a1eca40ff86abc14dc712def41798", + "sha256:d6e9611ae026be70604672cd71bee468cb231078d8d9b3d6b43f8dcbd4d9b776", + "sha256:d7c9255ba6626e1745bd68e1b85e3d7888844eaf38252fefb8157194e55fd3e9", + "sha256:e2f193a2076e4508a88c93c25348a1cea5e6b717ee50b4422a001e9ac819c3d5", + "sha256:e60674723cad7b7c7fc4e9075f7a9d5d927d23d290e30cf86aeb987ef135ca1d", + "sha256:ec45f2a5935b291d86974a24e09676e467ac108d0c7ce94de44d7650c43a5805" ], "index": "pypi", - "version": "==0.770" + "version": "==0.781" }, "mypy-extensions": { "hashes": [ @@ -317,16 +317,16 @@ }, "nodeenv": { "hashes": [ - "sha256:5b2438f2e42af54ca968dd1b374d14a1194848955187b0e5e4be1f73813a5212" + "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc" ], - "version": "==1.3.5" + "version": "==1.4.0" }, "packaging": { "hashes": [ - "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", - "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" + "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", + "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" ], - "version": "==20.3" + "version": "==20.4" }, "pathspec": { "hashes": [ @@ -351,25 +351,25 @@ }, "pre-commit": { "hashes": [ - "sha256:979b53dab1af35063a483bfe13b0fcbbf1a2cf8c46b60e0a9a8d08e8269647a1", - "sha256:f3e85e68c6d1cbe7828d3471896f1b192cfcf1c4d83bf26e26beeb5941855257" + "sha256:c5c8fd4d0e1c363723aaf0a8f9cba0f434c160b48c4028f4bae6d219177945b3", + "sha256:da463cf8f0e257f9af49047ba514f6b90dbd9b4f92f4c8847a3ccd36834874c7" ], "index": "pypi", - "version": "==2.3.0" + "version": "==2.5.1" }, "py": { "hashes": [ - "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", - "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" + "sha256:a673fa23d7000440cc885c17dbd34fafcb7d7a6e230b29f6766400de36a33c44", + "sha256:f3b3a4c36512a4c4f024041ab51866f11761cc169670204b235f6b20523d4e6b" ], - "version": "==1.8.1" + "version": "==1.8.2" }, "pycodestyle": { "hashes": [ - "sha256:933bfe8d45355fbb35f9017d81fc51df8cb7ce58b82aca2568b870bf7bea1611", - "sha256:c1362bf675a7c0171fa5f795917c570c2e405a97e5dc473b51f3656075d73acc" + "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", + "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], - "version": "==2.6.0a1" + "version": "==2.6.0" }, "pyflakes": { "hashes": [ @@ -387,11 +387,11 @@ }, "pylint": { "hashes": [ - "sha256:b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c", - "sha256:dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b" + "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", + "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" ], "index": "pypi", - "version": "==2.5.2" + "version": "==2.5.3" }, "pyparsing": { "hashes": [ @@ -402,27 +402,27 @@ }, "pytest": { "hashes": [ - "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3", - "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698" + "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1", + "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8" ], "index": "pypi", - "version": "==5.4.2" + "version": "==5.4.3" }, "pytest-cov": { "hashes": [ - "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", - "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626" + "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87", + "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c" ], "index": "pypi", - "version": "==2.8.1" + "version": "==2.10.0" }, "pytest-mock": { "hashes": [ - "sha256:997729451dfc36b851a9accf675488c7020beccda15e11c75632ee3d1b1ccd71", - "sha256:ce610831cedeff5331f4e2fc453a5dd65384303f680ab34bee2c6533855b431c" + "sha256:636e792f7dd9e2c80657e174c04bf7aa92672350090736d82e97e92ce8f68737", + "sha256:a9fedba70e37acf016238bb2293f2652ce19985ceb245bbd3d7f3e4032667402" ], "index": "pypi", - "version": "==3.1.0" + "version": "==3.1.1" }, "python-dateutil": { "hashes": [ @@ -456,37 +456,37 @@ }, "regex": { "hashes": [ - "sha256:021a0ae4d2baeeb60a3014805a2096cb329bd6d9f30669b7ad0da51a9cb73349", - "sha256:04d6e948ef34d3eac133bedc0098364a9e635a7914f050edb61272d2ddae3608", - "sha256:099568b372bda492be09c4f291b398475587d49937c659824f891182df728cdf", - "sha256:0ff50843535593ee93acab662663cb2f52af8e31c3f525f630f1dc6156247938", - "sha256:1b17bf37c2aefc4cac8436971fe6ee52542ae4225cfc7762017f7e97a63ca998", - "sha256:1e2255ae938a36e9bd7db3b93618796d90c07e5f64dd6a6750c55f51f8b76918", - "sha256:2bc6a17a7fa8afd33c02d51b6f417fc271538990297167f68a98cae1c9e5c945", - "sha256:3ab5e41c4ed7cd4fa426c50add2892eb0f04ae4e73162155cd668257d02259dd", - "sha256:3b059e2476b327b9794c792c855aa05531a3f3044737e455d283c7539bd7534d", - "sha256:4df91094ced6f53e71f695c909d9bad1cca8761d96fd9f23db12245b5521136e", - "sha256:5493a02c1882d2acaaf17be81a3b65408ff541c922bfd002535c5f148aa29f74", - "sha256:5b741ecc3ad3e463d2ba32dce512b412c319993c1bb3d999be49e6092a769fb2", - "sha256:652ab4836cd5531d64a34403c00ada4077bb91112e8bcdae933e2eae232cf4a8", - "sha256:669a8d46764a09f198f2e91fc0d5acdac8e6b620376757a04682846ae28879c4", - "sha256:73a10404867b835f1b8a64253e4621908f0d71150eb4e97ab2e7e441b53e9451", - "sha256:7ce4a213a96d6c25eeae2f7d60d4dad89ac2b8134ec3e69db9bc522e2c0f9388", - "sha256:8127ca2bf9539d6a64d03686fd9e789e8c194fc19af49b69b081f8c7e6ecb1bc", - "sha256:b5b5b2e95f761a88d4c93691716ce01dc55f288a153face1654f868a8034f494", - "sha256:b7c9f65524ff06bf70c945cd8d8d1fd90853e27ccf86026af2afb4d9a63d06b1", - "sha256:f7f2f4226db6acd1da228adf433c5c3792858474e49d80668ea82ac87cf74a03", - "sha256:fa09da4af4e5b15c0e8b4986a083f3fd159302ea115a6cc0649cd163435538b8" - ], - "version": "==2020.5.7" + "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a", + "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938", + "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29", + "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae", + "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387", + "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a", + "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf", + "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610", + "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9", + "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5", + "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3", + "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89", + "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded", + "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754", + "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f", + "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868", + "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd", + "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910", + "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3", + "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac", + "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c" + ], + "version": "==2020.6.8" }, "requests": { "hashes": [ - "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", - "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], "index": "pypi", - "version": "==2.23.0" + "version": "==2.24.0" }, "requests-mock": { "hashes": [ @@ -505,32 +505,32 @@ }, "six": { "hashes": [ - "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", - "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.14.0" + "version": "==1.15.0" }, "toml": { "hashes": [ - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], - "version": "==0.10.0" + "version": "==0.10.1" }, "tox": { "hashes": [ - "sha256:8d97bfaf70053ed3db56f57377288621f1bcc7621446d301927d18df93b1c4c3", - "sha256:af09c19478e8fc7ce7555b3d802ddf601b82684b874812c5857f774b8aee1b67" + "sha256:50a188b8e17580c1fb931f494a754e6507d4185f54fb18aca5ba3e12d2ffd55e", + "sha256:c696d36cd7c6a28ada2da780400e44851b20ee19ef08cfe73344a1dcebbbe9f3" ], "index": "pypi", - "version": "==3.15.0" + "version": "==3.15.2" }, "tqdm": { "hashes": [ - "sha256:4733c4a10d0f2a4d098d801464bdaf5240c7dadd2a7fde4ee93b0a0efd9fb25e", - "sha256:acdafb20f51637ca3954150d0405ff1a7edde0ff19e38fb99a80a66210d2a28f" + "sha256:07c06493f1403c1380b630ae3dcbe5ae62abcf369a93bbc052502279f189ab8c", + "sha256:cd140979c2bebd2311dfb14781d8f19bd5a9debb92dcab9f6ef899c987fcf71f" ], - "version": "==4.46.0" + "version": "==4.46.1" }, "twine": { "hashes": [ @@ -584,17 +584,17 @@ }, "virtualenv": { "hashes": [ - "sha256:b4c14d4d73a0c23db267095383c4276ef60e161f94fde0427f2f21a0132dde74", - "sha256:fd0e54dec8ac96c1c7c87daba85f0a59a7c37fe38748e154306ca21c73244637" + "sha256:5102fbf1ec57e80671ef40ed98a84e980a71194cedf30c87c2b25c3a9e0b0107", + "sha256:ccfb8e1e05a1174f7bd4c163700277ba730496094fe1a58bea9d4ac140a207c8" ], - "version": "==20.0.20" + "version": "==20.0.23" }, "wcwidth": { "hashes": [ - "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", - "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" + "sha256:79375666b9954d4a1a10739315816324c3e73110af9d0e102d906fdb0aec009f", + "sha256:8c6b5b6ee1360b842645f336d9e5d68c55817c26d3050f46b235ef2bc650e48f" ], - "version": "==0.1.9" + "version": "==0.2.4" }, "webencodings": { "hashes": [ diff --git a/tox.ini b/tox.ini index e58ac6d89..b0ee38daa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38,pypy3 +envlist = py37,py38,pypy3 isolated_build = True skip_missing_interpreters = True From 70ec752fd50e52a050e3fd6cfb07c2b247a1fdc6 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 8 Aug 2020 23:39:57 +0200 Subject: [PATCH 265/333] Fix token update issue with non-OAuth2 login --- src/pyatmo/auth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index af72c138e..f355d19fc 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -255,6 +255,8 @@ def __init__( # pylint: disable=super-init-not-called self._client_id = client_id self._client_secret = client_secret + + self.extra = {"client_id": self.client_id, "client_secret": self.client_secret} self._oauth = OAuth2Session(client=LegacyApplicationClient(client_id=client_id)) self._oauth.fetch_token( From 1d6758bfe20e0b32d725c7a8f4864b3c46e90b19 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sun, 9 Aug 2020 11:38:49 +0200 Subject: [PATCH 266/333] Update auth.py --- src/pyatmo/auth.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index f355d19fc..38ac4a28c 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -252,18 +252,16 @@ def __init__( password: str, scope="read_station", ): - # pylint: disable=super-init-not-called - self._client_id = client_id - self._client_secret = client_secret - - self.extra = {"client_id": self.client_id, "client_secret": self.client_secret} + super().__init__(client_id=client_id, client_secret=client_secret, scope=scope) - self._oauth = OAuth2Session(client=LegacyApplicationClient(client_id=client_id)) + self._oauth = OAuth2Session( + client=LegacyApplicationClient(client_id=self.client_id) + ) self._oauth.fetch_token( token_url=AUTH_REQ, username=username, password=password, - client_id=client_id, - client_secret=client_secret, - scope=scope, + client_id=self.client_id, + client_secret=self.client_secret, + scope=self.scope, ) From 523605bab7b7a8c5277da1e6762ae086d2963091 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 24 Sep 2020 14:48:05 +0200 Subject: [PATCH 267/333] Fix crash when station_name not present --- fixtures/weatherstation_data_simple.json | 46 ++++++++++++++++++++++++ src/pyatmo/weather_station.py | 8 +++-- tests/test_pyatmo_weatherstation.py | 10 ++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/fixtures/weatherstation_data_simple.json b/fixtures/weatherstation_data_simple.json index 6d5e1f78a..1e8651613 100644 --- a/fixtures/weatherstation_data_simple.json +++ b/fixtures/weatherstation_data_simple.json @@ -459,6 +459,52 @@ } } ] + }, + { + "_id": "12:34:56:1d:68:2e", + "date_setup": 1470935500, + "last_setup": 1470935500, + "type": "NAMain", + "last_status_store": 1588481399, + "module_name": "Basisstation", + "firmware": 177, + "last_upgrade": 1470935401, + "wifi_status": 13, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 93, + "city": "Gothenburg", + "country": "SE", + "timezone": "Europe/Stockholm", + "location": [ + 11.6136629, + 57.7006827 + ] + }, + "dashboard_data": { + "time_utc": 1588481387, + "Temperature": 20.8, + "CO2": 674, + "Humidity": 41, + "Noise": 34, + "Pressure": 1012.1, + "AbsolutePressure": 1001, + "min_temp": 20.8, + "max_temp": 22.2, + "date_max_temp": 1588456859, + "date_min_temp": 1588480176, + "temp_trend": "stable", + "pressure_trend": "up" + }, + "modules": [] } ], "user": { diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 38b6a656f..a1e8f3dc4 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -84,15 +84,17 @@ def get_modules(self, station_id: str) -> Dict: res = {} for station in [self.stations[station_data["_id"]]]: + station_type = station.get("type") + station_name = station.get("station_name", station_type) res[station["_id"]] = { - "station_name": station["station_name"], - "module_name": station.get("module_name", station.get("type")), + "station_name": station_name, + "module_name": station.get("module_name", station_type), "id": station["_id"], } for module in station["modules"]: res[module["_id"]] = { - "station_name": module.get("station_name", station["station_name"]), + "station_name": module.get("station_name", station_name), "module_name": module.get("module_name", module.get("type")), "id": module["_id"], } diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 69f7b88d7..363e64ca3 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -112,6 +112,16 @@ def test_weather_station_get_module_names(weather_station_data, station_id, expe }, }, ), + ( + "12:34:56:1d:68:2e", + { + "12:34:56:1d:68:2e": { + "id": "12:34:56:1d:68:2e", + "module_name": "Basisstation", + "station_name": "NAMain", + } + }, + ), pytest.param( "NoValidStation", None, From e0db01f41e7f032824fe8be112c5984d86c06685 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 24 Sep 2020 15:24:05 +0200 Subject: [PATCH 268/333] Blacken code --- src/pyatmo/auth.py | 5 +++- tests/test_pyatmo.py | 3 ++- tests/test_pyatmo_camera.py | 12 +++++++-- tests/test_pyatmo_thermostat.py | 25 +++++++++++++++--- tests/test_pyatmo_weatherstation.py | 40 ++++++++++++++++++++++++----- 5 files changed, 71 insertions(+), 14 deletions(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index af72c138e..00bcbd4f8 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -101,7 +101,10 @@ def refresh_tokens(self) -> Dict[str, Union[str, int]]: return token def post_request( - self, url: str, params: Optional[Dict] = None, timeout: int = 5, + self, + url: str, + params: Optional[Dict] = None, + timeout: int = 5, ) -> Any: """Wrapper for post requests.""" resp = None diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index f608c246b..cb8711da4 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -58,7 +58,8 @@ def test_post_request_binary(auth, requests_mock): @pytest.mark.parametrize( - "test_input,expected", [(200, None), (404, None), (401, None)], + "test_input,expected", + [(200, None), (404, None), (401, None)], ) def test_post_request_fail(auth, requests_mock, test_input, expected): """Test failing requests against the Netatmo API.""" diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 6a232ae6e..7263a8baf 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -112,7 +112,8 @@ def test_camera_data_camera_urls_disconnected(auth, requests_mock): @pytest.mark.parametrize( - "home_id, expected", [("91763b24c43d3e344f424e8b", ["Richard Doe"])], + "home_id, expected", + [("91763b24c43d3e344f424e8b", ["Richard Doe"])], ) def test_camera_data_persons_at_home(camera_home_data, home_id, expected): assert camera_home_data.persons_at_home(home_id) == expected @@ -321,7 +322,14 @@ def test_camera_data_get_smokedetector(camera_home_data, sid, expected): "camera_set_state_ok.json", True, ), - (None, "12:34:56:00:f1:62", None, "on", "camera_set_state_ok.json", True,), + ( + None, + "12:34:56:00:f1:62", + None, + "on", + "camera_set_state_ok.json", + True, + ), ( "91763b24c43d3e344f424e8b", "12:34:56:00:f1:62", diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index d9dd49d04..b71444bd6 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -391,7 +391,13 @@ def test_home_status_boiler_status(home_status): ], ) def test_home_status_set_thermmode( - home_status, requests_mock, mode, end_time, schedule_id, json_fixture, expected, + home_status, + requests_mock, + mode, + end_time, + schedule_id, + json_fixture, + expected, ): with open("fixtures/%s" % json_fixture) as json_file: json_fixture = json.load(json_file) @@ -451,7 +457,14 @@ def test_home_status_set_thermmode( ], ) def test_home_status_set_room_thermpoint( - home_status, requests_mock, room_id, mode, temp, end_time, json_fixture, expected, + home_status, + requests_mock, + room_id, + mode, + temp, + end_time, + json_fixture, + expected, ): with open("fixtures/%s" % json_fixture) as json_file: json_fixture = json.load(json_file) @@ -506,7 +519,13 @@ def test_home_status_set_room_thermpoint( ], ) def test_home_status_set_room_thermpoint_error( - home_status, requests_mock, room_id, mode, temp, json_fixture, expected, + home_status, + requests_mock, + room_id, + mode, + temp, + json_fixture, + expected, ): with open("fixtures/%s" % json_fixture) as json_file: json_fixture = json.load(json_file) diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 363e64ca3..6ca08324c 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -59,7 +59,10 @@ def test_weather_station_data_no_data(auth, requests_mock): "Yard", ], ), - ("12:34:56:36:fd:3c", ["Module", "NAMain", "Rain Gauge"],), + ( + "12:34:56:36:fd:3c", + ["Module", "NAMain", "Rain Gauge"], + ), pytest.param( "NoValidStation", None, @@ -76,7 +79,10 @@ def test_weather_station_get_module_names(weather_station_data, station_id, expe @pytest.mark.parametrize( "station_id, expected", [ - (None, {},), + ( + None, + {}, + ), ( "12:34:56:37:11:ca", { @@ -256,7 +262,11 @@ def test_weather_station_get_monitored_conditions( @pytest.mark.parametrize( "station_id, exclude, expected", [ - ("12:34:56:05:51:20", None, {},), + ( + "12:34:56:05:51:20", + None, + {}, + ), ( "12:34:56:37:11:ca", None, @@ -322,7 +332,11 @@ def test_weather_station_get_last_data( "12:34:56:37:11:ca", ], ), - ("12:34:56:37:11:ca", 798500, [],), + ( + "12:34:56:37:11:ca", + 798500, + [], + ), pytest.param( "NoValidStation", 3600, @@ -354,7 +368,11 @@ def test_weather_station_check_not_updated( "12:34:56:37:11:ca", ], ), - ("12:34:56:37:11:ca", 100, [],), + ( + "12:34:56:37:11:ca", + 100, + [], + ), ], ) def test_weather_station_check_updated( @@ -421,8 +439,16 @@ def test_weather_station_get_last_data_measurements(weather_station_data): "12:34:56:37:11:ca", ], ), - (None, None, {},), - ("12:34:56:00:aa:01", None, {},), + ( + None, + None, + {}, + ), + ( + "12:34:56:00:aa:01", + None, + {}, + ), ], ) def test_weather_station_get_last_data_bug_97( From 28b1c824dfb441ce3527cf9909243302f786b04a Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 6 Oct 2020 11:40:48 +0200 Subject: [PATCH 269/333] Add trends --- src/pyatmo/weather_station.py | 6 ++++++ tests/test_pyatmo_weatherstation.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index a1e8f3dc4..428b244fa 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -143,6 +143,12 @@ def get_monitored_conditions(self, module_id: str) -> List: if module["type"] in ["NAMain", "NAModule1", "NAModule4", "NHC"]: conditions.extend(["min_temp", "max_temp"]) + if module["type"] in ["NAMain", "NAModule1", "NAModule4"]: + conditions.extend(["temp_trend"]) + + if module["type"] == "NAMain": + conditions.extend(["pressure_trend"]) + if module["type"] in [ "NAMain", "NAModule1", diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 6ca08324c..08aeac50d 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -190,6 +190,7 @@ def test_weather_station_get_module(weather_station_data, mid, expected): "min_temp", "reachable", "rf_status", + "temp_trend", ], ), ( @@ -204,6 +205,7 @@ def test_weather_station_get_module(weather_station_data, mid, expected): "min_temp", "reachable", "rf_status", + "temp_trend", ], ), ( @@ -241,7 +243,9 @@ def test_weather_station_get_module(weather_station_data, mid, expected): "Temperature", "max_temp", "min_temp", + "pressure_trend", "reachable", + "temp_trend", "wifi_status", ], ), From 7351d4285636e86ce31d054e9937b8dd789ecda7 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Wed, 7 Oct 2020 21:26:40 +0200 Subject: [PATCH 270/333] Update tools and deps Signed-off-by: Hugo D. (jabesq) --- Pipfile.lock | 365 ++++++++++++++++++---------------- src/pyatmo/auth.py | 4 +- src/pyatmo/home_coach.py | 2 +- src/pyatmo/public_data.py | 4 +- src/pyatmo/weather_station.py | 6 +- 5 files changed, 200 insertions(+), 181 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index eed73cb4e..67448972d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -32,10 +32,10 @@ }, "idna": { "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.9" + "version": "==2.10" }, "oauthlib": { "hashes": [ @@ -66,10 +66,10 @@ }, "urllib3": { "hashes": [ - "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", - "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], - "version": "==1.25.9" + "version": "==1.25.10" } }, "develop": { @@ -89,26 +89,25 @@ }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], - "version": "==19.3.0" + "version": "==20.2.0" }, "black": { "hashes": [ - "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", - "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" + "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" ], "index": "pypi", - "version": "==19.10b0" + "version": "==20.8b1" }, "bleach": { "hashes": [ - "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f", - "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b" + "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", + "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" ], "index": "pypi", - "version": "==3.1.5" + "version": "==3.2.1" }, "certifi": { "hashes": [ @@ -119,10 +118,10 @@ }, "cfgv": { "hashes": [ - "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53", - "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513" + "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", + "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], - "version": "==3.1.0" + "version": "==3.2.0" }, "chardet": { "hashes": [ @@ -138,47 +137,58 @@ ], "version": "==7.1.2" }, + "colorama": { + "hashes": [ + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + ], + "version": "==0.4.3" + }, "coverage": { "hashes": [ - "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a", - "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355", - "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65", - "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7", - "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9", - "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1", - "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0", - "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55", - "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c", - "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6", - "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef", - "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019", - "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e", - "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0", - "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf", - "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24", - "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2", - "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c", - "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4", - "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0", - "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd", - "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04", - "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e", - "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730", - "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2", - "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768", - "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796", - "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7", - "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a", - "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489", - "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052" - ], - "version": "==5.1" + "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", + "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", + "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", + "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", + "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", + "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", + "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", + "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", + "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", + "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", + "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", + "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", + "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", + "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", + "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", + "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", + "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", + "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", + "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", + "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", + "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", + "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", + "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", + "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", + "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", + "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", + "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", + "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", + "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", + "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", + "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", + "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", + "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", + "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" + ], + "version": "==5.3" }, "distlib": { "hashes": [ - "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21" + "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", + "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1" ], - "version": "==0.3.0" + "version": "==0.3.1" }, "docutils": { "hashes": [ @@ -197,56 +207,63 @@ }, "flake8": { "hashes": [ - "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", - "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" + "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839", + "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b" ], "index": "pypi", - "version": "==3.8.3" + "version": "==3.8.4" }, "freezegun": { "hashes": [ - "sha256:82c757a05b7c7ca3e176bfebd7d6779fd9139c7cb4ef969c38a28d74deef89b2", - "sha256:e2062f2c7f95cc276a834c22f1a17179467176b624cc6f936e8bc3be5535ad1b" + "sha256:02b35de52f4699a78f6ac4518e4cd3390dddc43b0aeb978335a8f270a2d9668b", + "sha256:1cf08e441f913ff5e59b19cc065a8faa9dd1ddc442eaf0375294f344581a0643" ], "index": "pypi", - "version": "==0.3.15" + "version": "==1.0.0" }, "identify": { "hashes": [ - "sha256:249ebc7e2066d6393d27c1b1be3b70433f824a120b1d8274d362f1eb419e3b52", - "sha256:781fd3401f5d2b17b22a8b18b493a48d5d948e3330634e82742e23f9c20234ef" + "sha256:7c22c384a2c9b32c5cc891d13f923f6b2653aa83e2d75d8f79be240d6c86c4f4", + "sha256:da683bfb7669fa749fc7731f378229e2dbf29a1d1337cbde04106f02236eb29d" ], - "version": "==1.4.19" + "version": "==1.5.5" }, "idna": { "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.9" + "version": "==2.10" }, "importlib-metadata": { "hashes": [ - "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545", - "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958" + "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", + "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" ], "markers": "python_version < '3.8'", - "version": "==1.6.1" + "version": "==1.7.0" + }, + "iniconfig": { + "hashes": [ + "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", + "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69" + ], + "version": "==1.0.1" }, "isort": { "hashes": [ - "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", - "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + "sha256:36f0c6659b9000597e92618d05b72d4181104cf59472b1c6a039e3783f930c95", + "sha256:ba040c24d20aa302f78f4747df549573ae1eaf8e1084269199154da9c483f07f" ], "index": "pypi", - "version": "==4.3.21" + "version": "==5.5.4" }, "keyring": { "hashes": [ - "sha256:3401234209015144a5d75701e71cb47239e552b0882313e9f51e8976f9e27843", - "sha256:c53e0e5ccde3ad34284a40ce7976b5b3a3d6de70344c3f8ee44364cc340976ec" + "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", + "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466" ], - "version": "==21.2.1" + "version": "==21.4.0" }, "lazy-object-proxy": { "hashes": [ @@ -281,32 +298,25 @@ ], "version": "==0.6.1" }, - "more-itertools": { - "hashes": [ - "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5", - "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2" - ], - "version": "==8.4.0" - }, "mypy": { "hashes": [ - "sha256:1fe322a51df7ec60e4060c358422732359dda4467c1bd240f2172433b26b39be", - "sha256:2bc1fe8f793f1b1809afd6f57c2d9b948e1bf525a78e4c2957392a383c86a4a4", - "sha256:43335f3ff3288eb877de6d186ec08e10ea61093405189678756e14c7ccee4a9b", - "sha256:5c75603ea44bffad0356df333bd1b411facad321e0692d6b0687d65d94fcd8e1", - "sha256:738a8df84da4e9ce1f59cbdebc7d1d03310149405df9a95308d4f2a62a6d7c13", - "sha256:845abd8a9537da01e6b96f091e2d6d4ece18e52339f81f9fbbac1b6db2aa8d2d", - "sha256:94bb664868b5cf4ca1147d875a4c77883d8c605cf2e916853006e4c6194f1e84", - "sha256:b0c6ae0cb991a7a11013d0cc7edf8343184465b6e2dcbb9e44265c7bb3fde3af", - "sha256:ca56382e6ebdeb3cb928ae4ce87031cc752a1eca40ff86abc14dc712def41798", - "sha256:d6e9611ae026be70604672cd71bee468cb231078d8d9b3d6b43f8dcbd4d9b776", - "sha256:d7c9255ba6626e1745bd68e1b85e3d7888844eaf38252fefb8157194e55fd3e9", - "sha256:e2f193a2076e4508a88c93c25348a1cea5e6b717ee50b4422a001e9ac819c3d5", - "sha256:e60674723cad7b7c7fc4e9075f7a9d5d927d23d290e30cf86aeb987ef135ca1d", - "sha256:ec45f2a5935b291d86974a24e09676e467ac108d0c7ce94de44d7650c43a5805" + "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c", + "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86", + "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b", + "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd", + "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc", + "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea", + "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e", + "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308", + "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406", + "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d", + "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707", + "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d", + "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c", + "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a" ], "index": "pypi", - "version": "==0.781" + "version": "==0.782" }, "mypy-extensions": { "hashes": [ @@ -317,9 +327,10 @@ }, "nodeenv": { "hashes": [ - "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc" + "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9", + "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c" ], - "version": "==1.4.0" + "version": "==1.5.0" }, "packaging": { "hashes": [ @@ -351,18 +362,18 @@ }, "pre-commit": { "hashes": [ - "sha256:c5c8fd4d0e1c363723aaf0a8f9cba0f434c160b48c4028f4bae6d219177945b3", - "sha256:da463cf8f0e257f9af49047ba514f6b90dbd9b4f92f4c8847a3ccd36834874c7" + "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a", + "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70" ], "index": "pypi", - "version": "==2.5.1" + "version": "==2.7.1" }, "py": { "hashes": [ - "sha256:a673fa23d7000440cc885c17dbd34fafcb7d7a6e230b29f6766400de36a33c44", - "sha256:f3b3a4c36512a4c4f024041ab51866f11761cc169670204b235f6b20523d4e6b" + "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", + "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" ], - "version": "==1.8.2" + "version": "==1.9.0" }, "pycodestyle": { "hashes": [ @@ -380,49 +391,49 @@ }, "pygments": { "hashes": [ - "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", - "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" + "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998", + "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7" ], - "version": "==2.6.1" + "version": "==2.7.1" }, "pylint": { "hashes": [ - "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", - "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" + "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", + "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" ], "index": "pypi", - "version": "==2.5.3" + "version": "==2.6.0" }, "pyparsing": { "hashes": [ - "sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492", - "sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a" + "sha256:1060635ca5ac864c2b7bc7b05a448df4e32d7d8c65e33cbe1514810d339672a2", + "sha256:56a551039101858c9e189ac9e66e330a03fb7079e97ba6b50193643905f450ce" ], - "version": "==3.0.0a1" + "version": "==3.0.0a2" }, "pytest": { "hashes": [ - "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1", - "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8" + "sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9", + "sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92" ], "index": "pypi", - "version": "==5.4.3" + "version": "==6.1.1" }, "pytest-cov": { "hashes": [ - "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87", - "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c" + "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191", + "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e" ], "index": "pypi", - "version": "==2.10.0" + "version": "==2.10.1" }, "pytest-mock": { "hashes": [ - "sha256:636e792f7dd9e2c80657e174c04bf7aa92672350090736d82e97e92ce8f68737", - "sha256:a9fedba70e37acf016238bb2293f2652ce19985ceb245bbd3d7f3e4032667402" + "sha256:024e405ad382646318c4281948aadf6fe1135632bea9cc67366ea0c4098ef5f2", + "sha256:a4d6d37329e4a893e77d9ffa89e838dd2b45d5dc099984cf03c703ac8411bb82" ], "index": "pypi", - "version": "==3.1.1" + "version": "==3.3.1" }, "python-dateutil": { "hashes": [ @@ -456,29 +467,35 @@ }, "regex": { "hashes": [ - "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a", - "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938", - "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29", - "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae", - "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387", - "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a", - "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf", - "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610", - "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9", - "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5", - "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3", - "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89", - "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded", - "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754", - "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f", - "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868", - "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd", - "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910", - "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3", - "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac", - "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c" - ], - "version": "==2020.6.8" + "sha256:088afc8c63e7bd187a3c70a94b9e50ab3f17e1d3f52a32750b5b77dbe99ef5ef", + "sha256:1fe0a41437bbd06063aa184c34804efa886bcc128222e9916310c92cd54c3b4c", + "sha256:3d20024a70b97b4f9546696cbf2fd30bae5f42229fbddf8661261b1eaff0deb7", + "sha256:41bb65f54bba392643557e617316d0d899ed5b4946dccee1cb6696152b29844b", + "sha256:4318d56bccfe7d43e5addb272406ade7a2274da4b70eb15922a071c58ab0108c", + "sha256:4707f3695b34335afdfb09be3802c87fa0bc27030471dbc082f815f23688bc63", + "sha256:49f23ebd5ac073765ecbcf046edc10d63dcab2f4ae2bce160982cb30df0c0302", + "sha256:5533a959a1748a5c042a6da71fe9267a908e21eded7a4f373efd23a2cbdb0ecc", + "sha256:5d892a4f1c999834eaa3c32bc9e8b976c5825116cde553928c4c8e7e48ebda67", + "sha256:5f18875ac23d9aa2f060838e8b79093e8bb2313dbaaa9f54c6d8e52a5df097be", + "sha256:60b0e9e6dc45683e569ec37c55ac20c582973841927a85f2d8a7d20ee80216ab", + "sha256:816064fc915796ea1f26966163f6845de5af78923dfcecf6551e095f00983650", + "sha256:84cada8effefe9a9f53f9b0d2ba9b7b6f5edf8d2155f9fdbe34616e06ececf81", + "sha256:84e9407db1b2eb368b7ecc283121b5e592c9aaedbe8c78b1a2f1102eb2e21d19", + "sha256:8d69cef61fa50c8133382e61fd97439de1ae623fe943578e477e76a9d9471637", + "sha256:9a02d0ae31d35e1ec12a4ea4d4cca990800f66a917d0fb997b20fbc13f5321fc", + "sha256:9bc13e0d20b97ffb07821aa3e113f9998e84994fe4d159ffa3d3a9d1b805043b", + "sha256:a6f32aea4260dfe0e55dc9733ea162ea38f0ea86aa7d0f77b15beac5bf7b369d", + "sha256:ae91972f8ac958039920ef6e8769277c084971a142ce2b660691793ae44aae6b", + "sha256:c570f6fa14b9c4c8a4924aaad354652366577b4f98213cf76305067144f7b100", + "sha256:c9443124c67b1515e4fe0bb0aa18df640965e1030f468a2a5dc2589b26d130ad", + "sha256:d23a18037313714fb3bb5a94434d3151ee4300bae631894b1ac08111abeaa4a3", + "sha256:eaf548d117b6737df379fdd53bdde4f08870e66d7ea653e230477f071f861121", + "sha256:ebbe29186a3d9b0c591e71b7393f1ae08c83cb2d8e517d2a822b8f7ec99dfd8b", + "sha256:eda4771e0ace7f67f58bc5b560e27fb20f32a148cbc993b0c3835970935c2707", + "sha256:f1b3afc574a3db3b25c89161059d857bd4909a1269b0b3cb3c904677c8c4a3f7", + "sha256:f2388013e68e750eaa16ccbea62d4130180c26abb1d8e5d584b9baf69672b30f" + ], + "version": "==2020.9.27" }, "requests": { "hashes": [ @@ -503,6 +520,13 @@ ], "version": "==0.9.1" }, + "rfc3986": { + "hashes": [ + "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d", + "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50" + ], + "version": "==1.4.0" + }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", @@ -519,26 +543,26 @@ }, "tox": { "hashes": [ - "sha256:50a188b8e17580c1fb931f494a754e6507d4185f54fb18aca5ba3e12d2ffd55e", - "sha256:c696d36cd7c6a28ada2da780400e44851b20ee19ef08cfe73344a1dcebbbe9f3" + "sha256:e6318f404aff16522ff5211c88cab82b39af121735a443674e4e2e65f4e4637b", + "sha256:eb629ddc60e8542fd4a1956b2462e3b8771d49f1ff630cecceacaa0fbfb7605a" ], "index": "pypi", - "version": "==3.15.2" + "version": "==3.20.0" }, "tqdm": { "hashes": [ - "sha256:07c06493f1403c1380b630ae3dcbe5ae62abcf369a93bbc052502279f189ab8c", - "sha256:cd140979c2bebd2311dfb14781d8f19bd5a9debb92dcab9f6ef899c987fcf71f" + "sha256:5313148c57fcca7df562187903cf9cfa30fe1df2fe0641ea6ddb8ef9e841a137", + "sha256:b04bbbc52a7f1e3665eaa310f34c6ebbdf058bd3f6251fd64c6ab831817121ea" ], - "version": "==4.46.1" + "version": "==4.50.1" }, "twine": { "hashes": [ - "sha256:c1af8ca391e43b0a06bbc155f7f67db0bf0d19d284bfc88d1675da497a946124", - "sha256:d561a5e511f70275e5a485a6275ff61851c16ffcb3a95a602189161112d9f160" + "sha256:34352fd52ec3b9d29837e6072d5a2a7c6fe4290e97bba46bb8d478b5c598f7ab", + "sha256:ba9ff477b8d6de0c89dd450e70b2185da190514e91c42cc62f96850025c10472" ], "index": "pypi", - "version": "==3.1.1" + "version": "==3.2.0" }, "typed-ast": { "hashes": [ @@ -569,32 +593,25 @@ }, "typing-extensions": { "hashes": [ - "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", - "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", - "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], - "version": "==3.7.4.2" + "version": "==3.7.4.3" }, "urllib3": { "hashes": [ - "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", - "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], - "version": "==1.25.9" + "version": "==1.25.10" }, "virtualenv": { "hashes": [ - "sha256:5102fbf1ec57e80671ef40ed98a84e980a71194cedf30c87c2b25c3a9e0b0107", - "sha256:ccfb8e1e05a1174f7bd4c163700277ba730496094fe1a58bea9d4ac140a207c8" + "sha256:35ecdeb58cfc2147bb0706f7cdef69a8f34f1b81b6d49568174e277932908b8f", + "sha256:a5e0d253fe138097c6559c906c528647254f437d1019af9d5a477b09bfa7300f" ], - "version": "==20.0.23" - }, - "wcwidth": { - "hashes": [ - "sha256:79375666b9954d4a1a10739315816324c3e73110af9d0e102d906fdb0aec009f", - "sha256:8c6b5b6ee1360b842645f336d9e5d68c55817c26d3050f46b235ef2bc650e48f" - ], - "version": "==0.2.4" + "version": "==20.0.33" }, "webencodings": { "hashes": [ @@ -611,10 +628,10 @@ }, "zipp": { "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" + "sha256:64ad89efee774d1897a58607895d80789c59778ea02185dd846ac38394a8642b", + "sha256:eed8ec0b8d1416b2ca33516a37a08892442f3954dee131e92cfd92d8fe3e7066" ], - "version": "==3.1.0" + "version": "==3.3.0" } } } diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index c415e7863..7814db4dd 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -174,12 +174,12 @@ def query(url: str, params: Dict, timeout: int, retries: int) -> Any: f"when accessing '{url}'" ) - except JSONDecodeError: + except JSONDecodeError as exc: raise ApiError( f"{resp.status_code} - " f"{ERRORS.get(resp.status_code, '')} - " f"when accessing '{url}'" - ) + ) from exc try: if "application/json" in resp.headers.get("content-type", []): diff --git a/src/pyatmo/home_coach.py b/src/pyatmo/home_coach.py index 91ef55d04..181d811fd 100644 --- a/src/pyatmo/home_coach.py +++ b/src/pyatmo/home_coach.py @@ -19,4 +19,4 @@ def __init__(self, auth: NetatmoOAuth2) -> None: Raises: NoDevice: No devices found. """ - super(HomeCoachData, self).__init__(auth, url_req=_GETHOMECOACHDATA_REQ) + super().__init__(auth, url_req=_GETHOMECOACHDATA_REQ) diff --git a/src/pyatmo/public_data.py b/src/pyatmo/public_data.py index a044ecc3e..52969a003 100644 --- a/src/pyatmo/public_data.py +++ b/src/pyatmo/public_data.py @@ -66,8 +66,8 @@ def __init__( resp = self.auth.post_request(url=_GETPUBLIC_DATA, params=post_params) try: self.raw_data = resp["body"] - except (KeyError, TypeError): - raise NoDevice("No public weather data returned by Netatmo server") + except (KeyError, TypeError) as exc: + raise NoDevice("No public weather data returned by Netatmo server") from exc self.status = resp["status"] self.time_exec = to_time_string(resp["time_exec"]) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 428b244fa..0da590b13 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -34,9 +34,11 @@ def __init__(self, auth: NetatmoOAuth2, url_req: str = None) -> None: try: self.raw_data = fix_id(resp["body"].get("devices")) - except KeyError: + except KeyError as exc: LOG.debug("No in response %s", resp) - raise NoDevice("No weather station data returned by Netatmo server") + raise NoDevice( + "No weather station data returned by Netatmo server" + ) from exc if not self.raw_data: raise NoDevice("No weather station available") From ded431576f9f74685d0fa7c6c7fe213fd9c867f4 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Wed, 7 Oct 2020 21:27:13 +0200 Subject: [PATCH 271/333] Bump version to v4.1.0 Signed-off-by: Hugo D. (jabesq) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9ad5c633a..583128592 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="pyatmo", - version="4.0.0", # Should be updated with new versions + version="4.1.0", # Should be updated with new versions author="Hugo Dupras", author_email="jabesq@gmail.com", packages=find_packages(exclude=["tests"], where="src"), From 31ce508fb7c3c6c80033051c0afe82883996c8d4 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 12 Oct 2020 12:28:19 +0200 Subject: [PATCH 272/333] Set station name to home name when not available --- src/pyatmo/weather_station.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 0da590b13..0a4ee874d 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -47,6 +47,8 @@ def __init__(self, auth: NetatmoOAuth2, url_req: str = None) -> None: self.modules = {} for item in self.raw_data: + if "station_name" not in item: + item["station_name"] = item.get("home_name", item["type"]) if "modules" not in item: item["modules"] = [item] From 561a2c84e5b58df41f110c4d95c44d7380e9bd38 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 12 Oct 2020 12:29:07 +0200 Subject: [PATCH 273/333] Modify test data --- fixtures/weatherstation_data_simple.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fixtures/weatherstation_data_simple.json b/fixtures/weatherstation_data_simple.json index 1e8651613..0d186405c 100644 --- a/fixtures/weatherstation_data_simple.json +++ b/fixtures/weatherstation_data_simple.json @@ -272,7 +272,7 @@ }, { "_id": "12:34:56:32:a7:60", - "station_name": "Ateljen", + "home_name": "Ateljen", "date_setup": 1566714693, "last_setup": 1566714693, "type": "NAMain", From a287825a7e01a8e3f3b4c8bfb9eb2c48bcac024d Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 12 Oct 2020 12:38:08 +0200 Subject: [PATCH 274/333] Add reasoning --- src/pyatmo/weather_station.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 0a4ee874d..5f4f2100b 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -47,6 +47,7 @@ def __init__(self, auth: NetatmoOAuth2, url_req: str = None) -> None: self.modules = {} for item in self.raw_data: + # The station name is sometimes not contained in the backend data if "station_name" not in item: item["station_name"] = item.get("home_name", item["type"]) From 3c6ee0ee9780a76916e999595132c327c2def258 Mon Sep 17 00:00:00 2001 From: Sourcery AI <> Date: Mon, 12 Oct 2020 10:46:11 +0000 Subject: [PATCH 275/333] 'Refactored by Sourcery' --- src/pyatmo/weather_station.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 5f4f2100b..a77747b09 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -214,22 +214,16 @@ def get_last_data(self, station_id: str, exclude: int = 0) -> Dict: def check_not_updated(self, station_id: str, delay: int = 3600) -> List: """Check if a given station has not been updated.""" res = self.get_last_data(station_id) - ret = [] - for key, value in res.items(): - if time.time() - value["When"] > delay: - ret.append(key) - - return ret + return [ + key for key, value in res.items() if time.time() - value["When"] > delay + ] def check_updated(self, station_id: str, delay: int = 3600) -> List: """Check if a given station has been updated.""" res = self.get_last_data(station_id) - ret = [] - for key, value in res.items(): - if time.time() - value["When"] < delay: - ret.append(key) - - return ret + return [ + key for key, value in res.items() if time.time() - value["When"] < delay + ] def get_data( self, From 090c0815ffb2e42bc25d39b10430606c9498e29a Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 15 Oct 2020 00:08:44 +0200 Subject: [PATCH 276/333] Remove max and min --- src/pyatmo/weather_station.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index a77747b09..b25561f94 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -145,9 +145,6 @@ def get_monitored_conditions(self, module_id: str) -> List: # assume all other modules have rf_status, battery_vp, and battery_percent conditions.extend(["rf_status", "battery_vp", "battery_percent"]) - if module["type"] in ["NAMain", "NAModule1", "NAModule4", "NHC"]: - conditions.extend(["min_temp", "max_temp"]) - if module["type"] in ["NAMain", "NAModule1", "NAModule4"]: conditions.extend(["temp_trend"]) From 6e4158cab1fc02256f20831cc30d04e6402d0de6 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 19 Oct 2020 13:44:56 +0200 Subject: [PATCH 277/333] Fix tests --- tests/test_pyatmo_weatherstation.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 08aeac50d..8c615596b 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -186,8 +186,6 @@ def test_weather_station_get_module(weather_station_data, mid, expected): "Temperature", "battery_percent", "battery_vp", - "max_temp", - "min_temp", "reachable", "rf_status", "temp_trend", @@ -201,8 +199,6 @@ def test_weather_station_get_module(weather_station_data, mid, expected): "Temperature", "battery_percent", "battery_vp", - "max_temp", - "min_temp", "reachable", "rf_status", "temp_trend", @@ -241,8 +237,6 @@ def test_weather_station_get_module(weather_station_data, mid, expected): "Noise", "Pressure", "Temperature", - "max_temp", - "min_temp", "pressure_trend", "reachable", "temp_trend", @@ -417,8 +411,6 @@ def test_weather_station_get_last_data_measurements(weather_station_data): mod = weather_station_data.get_last_data(station_id, None) - assert mod[station_id]["min_temp"] == 23.4 - assert mod[station_id]["max_temp"] == 25.6 assert mod[station_id]["Temperature"] == 24.6 assert mod[station_id]["Pressure"] == 1017.3 assert mod[module_id]["WindAngle"] == 217 From 34b76b9cf64d93c7c2c35a28926fe54b96745e49 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Tue, 20 Oct 2020 10:28:13 +0200 Subject: [PATCH 278/333] Create dependabot.yml --- .github/dependabot.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..c1c40362c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every weekday + interval: "daily" + + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + # Check for pip updates on Sundays + day: "sunday" From fa7efcd2774ea854898e51c6e6bfb2b5cfc7d8ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2020 08:28:38 +0000 Subject: [PATCH 279/333] Bump actions/checkout from v1 to v2.3.3 Bumps [actions/checkout](https://github.com/actions/checkout) from v1 to v2.3.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v1...a81bbbf8298c0fa03ea29cdc473d45769f953675) Signed-off-by: dependabot[bot] --- .github/workflows/pythonpackage.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 89186a62a..dcf87af86 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -19,7 +19,7 @@ jobs: python-version: [3.6] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2.3.3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: @@ -37,7 +37,7 @@ jobs: python-version: [3.6] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2.3.3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: @@ -59,7 +59,7 @@ jobs: python-version: [3.6, 3.7, 3.8] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2.3.3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: From 28bb9a7666ab2f33d97b67f0d57bc14863d4ce72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2020 08:28:39 +0000 Subject: [PATCH 280/333] Bump actions/setup-python from v1 to v2.1.4 Bumps [actions/setup-python](https://github.com/actions/setup-python) from v1 to v2.1.4. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v1...41b7212b1668f5de9d65e9c82aa777e6bbedb3a8) Signed-off-by: dependabot[bot] --- .github/workflows/pythonpackage.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index dcf87af86..c34e43ba7 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@v2.3.3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2.1.4 with: python-version: ${{ matrix.python-version }} - name: Check with black @@ -39,7 +39,7 @@ jobs: steps: - uses: actions/checkout@v2.3.3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2.1.4 with: python-version: ${{ matrix.python-version }} - name: Lint with flake8 @@ -61,7 +61,7 @@ jobs: steps: - uses: actions/checkout@v2.3.3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2.1.4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 73f475e1a38cff379eaf7d6160d1f359673ea6ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2020 10:45:34 +0200 Subject: [PATCH 281/333] Bump isort from 5.5.4 to 5.6.4 (#131) Bumps [isort](https://github.com/pycqa/isort) from 5.5.4 to 5.6.4. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/5.5.4...5.6.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 209 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 150 insertions(+), 59 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 67448972d..5e2a2931d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -66,10 +66,10 @@ }, "urllib3": { "hashes": [ - "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", - "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", + "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], - "version": "==1.25.10" + "version": "==1.25.11" } }, "develop": { @@ -116,6 +116,47 @@ ], "version": "==2020.6.20" }, + "cffi": { + "hashes": [ + "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", + "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", + "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", + "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", + "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", + "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", + "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", + "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", + "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", + "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", + "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", + "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", + "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", + "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", + "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", + "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", + "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", + "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", + "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", + "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", + "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", + "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", + "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", + "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", + "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", + "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", + "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", + "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", + "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", + "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", + "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", + "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", + "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", + "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", + "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", + "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" + ], + "version": "==1.14.3" + }, "cfgv": { "hashes": [ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", @@ -139,10 +180,10 @@ }, "colorama": { "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "version": "==0.4.3" + "version": "==0.4.4" }, "coverage": { "hashes": [ @@ -183,6 +224,33 @@ ], "version": "==5.3" }, + "cryptography": { + "hashes": [ + "sha256:21b47c59fcb1c36f1113f3709d37935368e34815ea1d7073862e92f810dc7499", + "sha256:451cdf60be4dafb6a3b78802006a020e6cd709c22d240f94f7a0696240a17154", + "sha256:4549b137d8cbe3c2eadfa56c0c858b78acbeff956bd461e40000b2164d9167c6", + "sha256:48ee615a779ffa749d7d50c291761dc921d93d7cf203dca2db663b4f193f0e49", + "sha256:559d622aef2a2dff98a892eef321433ba5bc55b2485220a8ca289c1ecc2bd54f", + "sha256:5d52c72449bb02dd45a773a203196e6d4fae34e158769c896012401f33064396", + "sha256:65beb15e7f9c16e15934569d29fb4def74ea1469d8781f6b3507ab896d6d8719", + "sha256:680da076cad81cdf5ffcac50c477b6790be81768d30f9da9e01960c4b18a66db", + "sha256:762bc5a0df03c51ee3f09c621e1cee64e3a079a2b5020de82f1613873d79ee70", + "sha256:89aceb31cd5f9fc2449fe8cf3810797ca52b65f1489002d58fe190bfb265c536", + "sha256:983c0c3de4cb9fcba68fd3f45ed846eb86a2a8b8d8bc5bb18364c4d00b3c61fe", + "sha256:99d4984aabd4c7182050bca76176ce2dbc9fa9748afe583a7865c12954d714ba", + "sha256:9d9fc6a16357965d282dd4ab6531013935425d0dc4950df2e0cf2a1b1ac1017d", + "sha256:a7597ffc67987b37b12e09c029bd1dc43965f75d328076ae85721b84046e9ca7", + "sha256:ab010e461bb6b444eaf7f8c813bb716be2d78ab786103f9608ffd37a4bd7d490", + "sha256:b12e715c10a13ca1bd27fbceed9adc8c5ff640f8e1f7ea76416352de703523c8", + "sha256:b2bded09c578d19e08bd2c5bb8fed7f103e089752c9cf7ca7ca7de522326e921", + "sha256:b372026ebf32fe2523159f27d9f0e9f485092e43b00a5adacf732192a70ba118", + "sha256:cb179acdd4ae1e4a5a160d80b87841b3d0e0be84af46c7bb2cd7ece57a39c4ba", + "sha256:e97a3b627e3cb63c415a16245d6cef2139cca18bb1183d1b9375a1c14e83f3b3", + "sha256:f0e099fc4cc697450c3dd4031791559692dd941a95254cb9aeded66a7aa8b9bc", + "sha256:f99317a0fa2e49917689b8cf977510addcfaaab769b3f899b9c481bbd76730c2" + ], + "version": "==3.1.1" + }, "distlib": { "hashes": [ "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", @@ -223,10 +291,10 @@ }, "identify": { "hashes": [ - "sha256:7c22c384a2c9b32c5cc891d13f923f6b2653aa83e2d75d8f79be240d6c86c4f4", - "sha256:da683bfb7669fa749fc7731f378229e2dbf29a1d1337cbde04106f02236eb29d" + "sha256:3139bf72d81dfd785b0a464e2776bd59bdc725b4cc10e6cf46b56a0db931c82e", + "sha256:969d844b7a85d32a5f9ac4e163df6e846d73c87c8b75847494ee8f4bd2186421" ], - "version": "==1.5.5" + "version": "==1.5.6" }, "idna": { "hashes": [ @@ -245,18 +313,26 @@ }, "iniconfig": { "hashes": [ - "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", - "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69" + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" ], - "version": "==1.0.1" + "version": "==1.1.1" }, "isort": { "hashes": [ - "sha256:36f0c6659b9000597e92618d05b72d4181104cf59472b1c6a039e3783f930c95", - "sha256:ba040c24d20aa302f78f4747df549573ae1eaf8e1084269199154da9c483f07f" + "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7", + "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58" ], "index": "pypi", - "version": "==5.5.4" + "version": "==5.6.4" + }, + "jeepney": { + "hashes": [ + "sha256:3479b861cc2b6407de5188695fa1a8d57e5072d7059322469b62628869b8e36e", + "sha256:d6c6b49683446d2407d2fe3acb7a368a77ff063f9182fe427da15d622adc24cf" + ], + "markers": "sys_platform == 'linux'", + "version": "==0.4.3" }, "keyring": { "hashes": [ @@ -382,6 +458,13 @@ ], "version": "==2.6.0" }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "version": "==2.20" + }, "pyflakes": { "hashes": [ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", @@ -460,42 +543,42 @@ }, "readme-renderer": { "hashes": [ - "sha256:cbe9db71defedd2428a1589cdc545f9bd98e59297449f69d721ef8f1cfced68d", - "sha256:cc4957a803106e820d05d14f71033092537a22daa4f406dfbdd61177e0936376" + "sha256:3176d93d2c21960fb7f7458073b9f1e5dd14a5af0ec6af4afb957337dbb6a326", + "sha256:e6871b10341cdd85ade112fb8503b31301d1ca12c0c3b3e1358855329519968b" ], - "version": "==26.0" + "version": "==27.0" }, "regex": { "hashes": [ - "sha256:088afc8c63e7bd187a3c70a94b9e50ab3f17e1d3f52a32750b5b77dbe99ef5ef", - "sha256:1fe0a41437bbd06063aa184c34804efa886bcc128222e9916310c92cd54c3b4c", - "sha256:3d20024a70b97b4f9546696cbf2fd30bae5f42229fbddf8661261b1eaff0deb7", - "sha256:41bb65f54bba392643557e617316d0d899ed5b4946dccee1cb6696152b29844b", - "sha256:4318d56bccfe7d43e5addb272406ade7a2274da4b70eb15922a071c58ab0108c", - "sha256:4707f3695b34335afdfb09be3802c87fa0bc27030471dbc082f815f23688bc63", - "sha256:49f23ebd5ac073765ecbcf046edc10d63dcab2f4ae2bce160982cb30df0c0302", - "sha256:5533a959a1748a5c042a6da71fe9267a908e21eded7a4f373efd23a2cbdb0ecc", - "sha256:5d892a4f1c999834eaa3c32bc9e8b976c5825116cde553928c4c8e7e48ebda67", - "sha256:5f18875ac23d9aa2f060838e8b79093e8bb2313dbaaa9f54c6d8e52a5df097be", - "sha256:60b0e9e6dc45683e569ec37c55ac20c582973841927a85f2d8a7d20ee80216ab", - "sha256:816064fc915796ea1f26966163f6845de5af78923dfcecf6551e095f00983650", - "sha256:84cada8effefe9a9f53f9b0d2ba9b7b6f5edf8d2155f9fdbe34616e06ececf81", - "sha256:84e9407db1b2eb368b7ecc283121b5e592c9aaedbe8c78b1a2f1102eb2e21d19", - "sha256:8d69cef61fa50c8133382e61fd97439de1ae623fe943578e477e76a9d9471637", - "sha256:9a02d0ae31d35e1ec12a4ea4d4cca990800f66a917d0fb997b20fbc13f5321fc", - "sha256:9bc13e0d20b97ffb07821aa3e113f9998e84994fe4d159ffa3d3a9d1b805043b", - "sha256:a6f32aea4260dfe0e55dc9733ea162ea38f0ea86aa7d0f77b15beac5bf7b369d", - "sha256:ae91972f8ac958039920ef6e8769277c084971a142ce2b660691793ae44aae6b", - "sha256:c570f6fa14b9c4c8a4924aaad354652366577b4f98213cf76305067144f7b100", - "sha256:c9443124c67b1515e4fe0bb0aa18df640965e1030f468a2a5dc2589b26d130ad", - "sha256:d23a18037313714fb3bb5a94434d3151ee4300bae631894b1ac08111abeaa4a3", - "sha256:eaf548d117b6737df379fdd53bdde4f08870e66d7ea653e230477f071f861121", - "sha256:ebbe29186a3d9b0c591e71b7393f1ae08c83cb2d8e517d2a822b8f7ec99dfd8b", - "sha256:eda4771e0ace7f67f58bc5b560e27fb20f32a148cbc993b0c3835970935c2707", - "sha256:f1b3afc574a3db3b25c89161059d857bd4909a1269b0b3cb3c904677c8c4a3f7", - "sha256:f2388013e68e750eaa16ccbea62d4130180c26abb1d8e5d584b9baf69672b30f" - ], - "version": "==2020.9.27" + "sha256:02686a2f0b1a4be0facdd0d3ad4dc6c23acaa0f38fb5470d892ae88584ba705c", + "sha256:137da580d1e6302484be3ef41d72cf5c3ad22a076070051b7449c0e13ab2c482", + "sha256:20cdd7e1736f4f61a5161aa30d05ac108ab8efc3133df5eb70fe1e6a23ea1ca6", + "sha256:25991861c6fef1e5fd0a01283cf5658c5e7f7aa644128e85243bc75304e91530", + "sha256:26b85672275d8c7a9d4ff93dbc4954f5146efdb2ecec89ad1de49439984dea14", + "sha256:2f60ba5c33f00ce9be29a140e6f812e39880df8ba9cb92ad333f0016dbc30306", + "sha256:3dd952f3f8dc01b72c0cf05b3631e05c50ac65ddd2afdf26551638e97502107b", + "sha256:578ac6379e65eb8e6a85299b306c966c852712c834dc7eef0ba78d07a828f67b", + "sha256:5d4a3221f37520bb337b64a0632716e61b26c8ae6aaffceeeb7ad69c009c404b", + "sha256:608d6c05452c0e6cc49d4d7407b4767963f19c4d2230fa70b7201732eedc84f2", + "sha256:65b6b018b07e9b3b6a05c2c3bb7710ed66132b4df41926c243887c4f1ff303d5", + "sha256:698f8a5a2815e1663d9895830a063098ae2f8f2655ae4fdc5dfa2b1f52b90087", + "sha256:6c72adb85adecd4522a488a751e465842cdd2a5606b65464b9168bf029a54272", + "sha256:6d4cdb6c20e752426b2e569128488c5046fb1b16b1beadaceea9815c36da0847", + "sha256:6e9f72e0ee49f7d7be395bfa29e9533f0507a882e1e6bf302c0a204c65b742bf", + "sha256:828618f3c3439c5e6ef8621e7c885ca561bbaaba90ddbb6a7dfd9e1ec8341103", + "sha256:85b733a1ef2b2e7001aff0e204a842f50ad699c061856a214e48cfb16ace7d0c", + "sha256:8958befc139ac4e3f16d44ec386c490ea2121ed8322f4956f83dd9cad8e9b922", + "sha256:a51e51eecdac39a50ede4aeed86dbef4776e3b73347d31d6ad0bc9648ba36049", + "sha256:aeac7c9397480450016bc4a840eefbfa8ca68afc1e90648aa6efbfe699e5d3bb", + "sha256:aef23aed9d4017cc74d37f703d57ce254efb4c8a6a01905f40f539220348abf9", + "sha256:af1f5e997dd1ee71fb6eb4a0fb6921bf7a778f4b62f1f7ef0d7445ecce9155d6", + "sha256:b5eeaf4b5ef38fab225429478caf71f44d4a0b44d39a1aa4d4422cda23a9821b", + "sha256:d25f5cca0f3af6d425c9496953445bf5b288bb5b71afc2b8308ad194b714c159", + "sha256:d81be22d5d462b96a2aa5c512f741255ba182995efb0114e5a946fe254148df1", + "sha256:e935a166a5f4c02afe3f7e4ce92ce5a786f75c6caa0c4ce09c922541d74b77e8", + "sha256:ef3a55b16c6450574734db92e0a3aca283290889934a23f7498eaf417e3af9f0" + ], + "version": "==2020.10.15" }, "requests": { "hashes": [ @@ -527,6 +610,14 @@ ], "version": "==1.4.0" }, + "secretstorage": { + "hashes": [ + "sha256:15da8a989b65498e29be338b3b279965f1b8f09b9668bd8010da183024c8bff6", + "sha256:b5ec909dde94d4ae2fa26af7c089036997030f0cf0a5cb372b4cccabd81c143b" + ], + "markers": "sys_platform == 'linux'", + "version": "==3.1.2" + }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", @@ -551,10 +642,10 @@ }, "tqdm": { "hashes": [ - "sha256:5313148c57fcca7df562187903cf9cfa30fe1df2fe0641ea6ddb8ef9e841a137", - "sha256:b04bbbc52a7f1e3665eaa310f34c6ebbdf058bd3f6251fd64c6ab831817121ea" + "sha256:43ca183da3367578ebf2f1c2e3111d51ea161ed1dc4e6345b86e27c2a93beff7", + "sha256:69dfa6714dee976e2425a9aab84b622675b7b1742873041e3db8a8e86132a4af" ], - "version": "==4.50.1" + "version": "==4.50.2" }, "twine": { "hashes": [ @@ -601,17 +692,17 @@ }, "urllib3": { "hashes": [ - "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", - "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", + "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], - "version": "==1.25.10" + "version": "==1.25.11" }, "virtualenv": { "hashes": [ - "sha256:35ecdeb58cfc2147bb0706f7cdef69a8f34f1b81b6d49568174e277932908b8f", - "sha256:a5e0d253fe138097c6559c906c528647254f437d1019af9d5a477b09bfa7300f" + "sha256:0ebc633426d7468664067309842c81edab11ae97fcaf27e8ad7f5748c89b431b", + "sha256:2a72c80fa2ad8f4e2985c06e6fc12c3d60d060e410572f553c90619b0f6efaf3" ], - "version": "==20.0.33" + "version": "==20.0.35" }, "webencodings": { "hashes": [ @@ -628,10 +719,10 @@ }, "zipp": { "hashes": [ - "sha256:64ad89efee774d1897a58607895d80789c59778ea02185dd846ac38394a8642b", - "sha256:eed8ec0b8d1416b2ca33516a37a08892442f3954dee131e92cfd92d8fe3e7066" + "sha256:16522f69653f0d67be90e8baa4a46d66389145b734345d68a257da53df670903", + "sha256:c1532a8030c32fd52ff6a288d855fe7adef5823ba1d26a29a68fd6314aa72baa" ], - "version": "==3.3.0" + "version": "==3.3.1" } } } From 636afe033c9109c18d21c5edcf99e0ee824a8085 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2020 10:45:48 +0200 Subject: [PATCH 282/333] Bump tox from 3.20.0 to 3.20.1 (#130) Bumps [tox](https://github.com/tox-dev/tox) from 3.20.0 to 3.20.1. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/master/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/3.20.0...3.20.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 5e2a2931d..a0a85f31b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -305,11 +305,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", - "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" + "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da", + "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3" ], "markers": "python_version < '3.8'", - "version": "==1.7.0" + "version": "==2.0.0" }, "iniconfig": { "hashes": [ @@ -334,6 +334,14 @@ "markers": "sys_platform == 'linux'", "version": "==0.4.3" }, + "jeepney": { + "hashes": [ + "sha256:3479b861cc2b6407de5188695fa1a8d57e5072d7059322469b62628869b8e36e", + "sha256:d6c6b49683446d2407d2fe3acb7a368a77ff063f9182fe427da15d622adc24cf" + ], + "markers": "sys_platform == 'linux'", + "version": "==0.4.3" + }, "keyring": { "hashes": [ "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", @@ -634,11 +642,11 @@ }, "tox": { "hashes": [ - "sha256:e6318f404aff16522ff5211c88cab82b39af121735a443674e4e2e65f4e4637b", - "sha256:eb629ddc60e8542fd4a1956b2462e3b8771d49f1ff630cecceacaa0fbfb7605a" + "sha256:42ce19ce5dc2f6d6b1fdc5666c476e1f1e2897359b47e0aa3a5b774f335d57c2", + "sha256:4321052bfe28f9d85082341ca8e233e3ea901fdd14dab8a5d3fbd810269fbaf6" ], "index": "pypi", - "version": "==3.20.0" + "version": "==3.20.1" }, "tqdm": { "hashes": [ From 907bdaa5bac7ab59bb85586a6c996cc49882df05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2020 10:46:00 +0200 Subject: [PATCH 283/333] Bump mypy from 0.782 to 0.790 (#129) Bumps [mypy](https://github.com/python/mypy) from 0.782 to 0.790. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.782...v0.790) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index a0a85f31b..84fd4e18c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -342,6 +342,14 @@ "markers": "sys_platform == 'linux'", "version": "==0.4.3" }, + "jeepney": { + "hashes": [ + "sha256:3479b861cc2b6407de5188695fa1a8d57e5072d7059322469b62628869b8e36e", + "sha256:d6c6b49683446d2407d2fe3acb7a368a77ff063f9182fe427da15d622adc24cf" + ], + "markers": "sys_platform == 'linux'", + "version": "==0.4.3" + }, "keyring": { "hashes": [ "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", @@ -384,23 +392,23 @@ }, "mypy": { "hashes": [ - "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c", - "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86", - "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b", - "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd", - "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc", - "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea", - "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e", - "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308", - "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406", - "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d", - "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707", - "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d", - "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c", - "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a" + "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324", + "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc", + "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802", + "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122", + "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975", + "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7", + "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666", + "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669", + "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178", + "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01", + "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea", + "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de", + "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1", + "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c" ], "index": "pypi", - "version": "==0.782" + "version": "==0.790" }, "mypy-extensions": { "hashes": [ From 39383eae304d91e97e77245d6e63f3400026b976 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Tue, 20 Oct 2020 17:15:52 +0200 Subject: [PATCH 284/333] Realign Pipfile.lock Signed-off-by: Hugo Dupras --- Pipfile.lock | 74 +++++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 84fd4e18c..bfe27cd42 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -35,6 +35,7 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "oauthlib": { @@ -42,6 +43,7 @@ "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.1.0" }, "pyatmo": { @@ -59,7 +61,8 @@ "requests-oauthlib": { "hashes": [ "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", - "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" + "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", + "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" ], "index": "pypi", "version": "==1.3.0" @@ -69,6 +72,7 @@ "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.25.11" } }, @@ -85,6 +89,7 @@ "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], + "markers": "python_version >= '3.5'", "version": "==2.4.2" }, "attrs": { @@ -92,6 +97,7 @@ "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.2.0" }, "black": { @@ -162,6 +168,7 @@ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], + "markers": "python_full_version >= '3.6.1'", "version": "==3.2.0" }, "chardet": { @@ -176,6 +183,7 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "colorama": { @@ -183,6 +191,7 @@ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.4.4" }, "coverage": { @@ -222,6 +231,7 @@ "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==5.3" }, "cryptography": { @@ -249,6 +259,7 @@ "sha256:f0e099fc4cc697450c3dd4031791559692dd941a95254cb9aeded66a7aa8b9bc", "sha256:f99317a0fa2e49917689b8cf977510addcfaaab769b3f899b9c481bbd76730c2" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.1.1" }, "distlib": { @@ -294,6 +305,7 @@ "sha256:3139bf72d81dfd785b0a464e2776bd59bdc725b4cc10e6cf46b56a0db931c82e", "sha256:969d844b7a85d32a5f9ac4e163df6e846d73c87c8b75847494ee8f4bd2186421" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.5.6" }, "idna": { @@ -301,16 +313,9 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, - "importlib-metadata": { - "hashes": [ - "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da", - "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3" - ], - "markers": "python_version < '3.8'", - "version": "==2.0.0" - }, "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", @@ -334,27 +339,12 @@ "markers": "sys_platform == 'linux'", "version": "==0.4.3" }, - "jeepney": { - "hashes": [ - "sha256:3479b861cc2b6407de5188695fa1a8d57e5072d7059322469b62628869b8e36e", - "sha256:d6c6b49683446d2407d2fe3acb7a368a77ff063f9182fe427da15d622adc24cf" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.4.3" - }, - "jeepney": { - "hashes": [ - "sha256:3479b861cc2b6407de5188695fa1a8d57e5072d7059322469b62628869b8e36e", - "sha256:d6c6b49683446d2407d2fe3acb7a368a77ff063f9182fe427da15d622adc24cf" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.4.3" - }, "keyring": { "hashes": [ "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466" ], + "markers": "python_version >= '3.6'", "version": "==21.4.0" }, "lazy-object-proxy": { @@ -381,6 +371,7 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -429,6 +420,7 @@ "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.4" }, "pathspec": { @@ -440,16 +432,17 @@ }, "pkginfo": { "hashes": [ - "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", - "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" + "sha256:78d032b5888ec06d7f9d18fbf8c0549a6a3477081b34cb769119a07183624fc1", + "sha256:dd008e95b13141ddd05d7e8881f0c0366a998ab90b25c2db794a1714b71583cc" ], - "version": "==1.5.0.1" + "version": "==1.6.0" }, "pluggy": { "hashes": [ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.13.1" }, "pre-commit": { @@ -465,6 +458,7 @@ "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.9.0" }, "pycodestyle": { @@ -472,6 +466,7 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pycparser": { @@ -479,6 +474,7 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pyflakes": { @@ -486,6 +482,7 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pygments": { @@ -493,6 +490,7 @@ "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998", "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7" ], + "markers": "python_version >= '3.5'", "version": "==2.7.1" }, "pylint": { @@ -505,10 +503,11 @@ }, "pyparsing": { "hashes": [ - "sha256:1060635ca5ac864c2b7bc7b05a448df4e32d7d8c65e33cbe1514810d339672a2", - "sha256:56a551039101858c9e189ac9e66e330a03fb7079e97ba6b50193643905f450ce" + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "version": "==3.0.0a2" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.4.7" }, "pytest": { "hashes": [ @@ -539,6 +538,7 @@ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.1" }, "pyyaml": { @@ -639,6 +639,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.15.0" }, "toml": { @@ -661,6 +662,7 @@ "sha256:43ca183da3367578ebf2f1c2e3111d51ea161ed1dc4e6345b86e27c2a93beff7", "sha256:69dfa6714dee976e2425a9aab84b622675b7b1742873041e3db8a8e86132a4af" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1'", "version": "==4.50.2" }, "twine": { @@ -695,7 +697,6 @@ "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], - "markers": "implementation_name == 'cpython' and python_version < '3.8'", "version": "==1.4.1" }, "typing-extensions": { @@ -711,6 +712,7 @@ "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.25.11" }, "virtualenv": { @@ -718,6 +720,7 @@ "sha256:0ebc633426d7468664067309842c81edab11ae97fcaf27e8ad7f5748c89b431b", "sha256:2a72c80fa2ad8f4e2985c06e6fc12c3d60d060e410572f553c90619b0f6efaf3" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.0.35" }, "webencodings": { @@ -732,13 +735,6 @@ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" ], "version": "==1.12.1" - }, - "zipp": { - "hashes": [ - "sha256:16522f69653f0d67be90e8baa4a46d66389145b734345d68a257da53df670903", - "sha256:c1532a8030c32fd52ff6a288d855fe7adef5823ba1d26a29a68fd6314aa72baa" - ], - "version": "==3.3.1" } } } From f87a3a67503d1de85cf9d24500ebf63ee64d2551 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Mon, 12 Oct 2020 15:32:43 +0200 Subject: [PATCH 285/333] Upgrade a setup.py to declarative metadata using setup-py-upgrade Setup-py-upgrade is available here: https://github.com/asottile/setup-py-upgrade Signed-off-by: Hugo Dupras --- setup.cfg | 33 ++++++++++++++++++++++++++++++++- setup.py | 24 ++---------------------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/setup.cfg b/setup.cfg index 3006ec719..04bc68dc3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,36 @@ [metadata] -description-file = README.md +name = pyatmo +version = 4.1.0 +description = Simple API to access Netatmo weather station data from any Python 3 script. Design for Home-Assitant (but not only) +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/jabesq/netatmo-api-python +author = Hugo Dupras +author_email = jabesq@gmail.com +license = MIT +license_file = LICENSE.txt +classifiers = + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + +[options] +packages = find: +install_requires = + oauthlib + requests + requests_oauthlib +python_requires = >=3.7 +package_dir = =src + +[options.packages.find] +exclude = tests +where = src [flake8] max-line-length = 88 diff --git a/setup.py b/setup.py index 583128592..606849326 100755 --- a/setup.py +++ b/setup.py @@ -1,23 +1,3 @@ -# python setup.py --dry-run --verbose install -# To build: python setup.py sdist bdist_wheel +from setuptools import setup -from setuptools import find_packages, setup - -setup( - name="pyatmo", - version="4.1.0", # Should be updated with new versions - author="Hugo Dupras", - author_email="jabesq@gmail.com", - packages=find_packages(exclude=["tests"], where="src"), - package_dir={"": "src"}, - scripts=[], - data_files=[], - url="https://github.com/jabesq/netatmo-api-python", - license="MIT", - description=( - "Simple API to access Netatmo weather station data from any Python 3 script. " - "Design for Home-Assitant (but not only)" - ), - long_description=open("README.md").read(), - install_requires=["requests", "requests_oauthlib", "oauthlib"], -) +setup() From 15549637c8ac1a0572056d9437234e2a6e108fc1 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Mon, 12 Oct 2020 15:37:51 +0200 Subject: [PATCH 286/333] Add new tools to pre-commit Signed-off-by: Hugo Dupras --- .github/workflows/pythonpackage.yml | 4 +- .pre-commit-config.yaml | 60 ++++++++++++++++++++++++++--- README.md | 2 +- setup.cfg | 14 +++---- src/pyatmo/__main__.py | 2 +- src/pyatmo/auth.py | 18 +++++---- src/pyatmo/camera.py | 27 +++++++++---- src/pyatmo/thermostat.py | 11 +++++- src/pyatmo/weather_station.py | 9 +++-- tests/test_pyatmo_camera.py | 49 +++++++++++++++++++---- tests/test_pyatmo_homecoach.py | 6 +-- tests/test_pyatmo_publicdata.py | 7 +++- tests/test_pyatmo_thermostat.py | 19 +++++++-- tests/test_pyatmo_weatherstation.py | 40 ++++++++++++++----- usage.md | 3 +- 15 files changed, 206 insertions(+), 65 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index c34e43ba7..6df166195 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -1,6 +1,6 @@ name: Python package -on: +on: push: branches: - master @@ -49,7 +49,7 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - + build: runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14804b137..583cc8f91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,30 @@ default_stages: [commit, push] exclude: ^(fixtures/) repos: + - repo: https://github.com/asottile/seed-isort-config + rev: v2.2.0 + hooks: + - id: seed-isort-config + args: [--application-directories,./src] + + - repo: https://github.com/asottile/pyupgrade + rev: v2.7.2 + hooks: + - id: pyupgrade + args: [--py37-plus] + exclude: 'external_src/int-tools' + + - repo: https://github.com/asottile/add-trailing-comma + rev: v2.0.1 + hooks: + - id: add-trailing-comma + args: [ --py36-plus ] + exclude: 'external_src/int-tools' + + - repo: https://github.com/asottile/yesqa + rev: v1.2.2 + hooks: + - id: yesqa - repo: local hooks: - id: isort @@ -11,24 +35,50 @@ repos: entry: pipenv run isort types: [python] exclude: tests/ + - id: black name: black language: system entry: pipenv run black types: [python] + - id: pylint name: pylint language: system entry: pipenv run pylint types: [python] - - id: flake8 - name: flake8 - language: system - entry: pipenv run flake8 - types: [python] + - id: mypy name: mypy language: system entry: pipenv run mypy types: [python] exclude: tests/ + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 # Use the ref you want to point at + hooks: + - id: check-ast + - id: no-commit-to-branch + args: [--branch, master, --branch, devel] + - id: forbid-new-submodules + - id: check-merge-conflict + - id: detect-private-key + - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix=lf] + - id: trailing-whitespace + - id: debug-statements + - id: check-toml + + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 # pick a git hash / tag to point to + hooks: + - id: flake8 + exclude: (otp) + additional_dependencies: [flake8-typing-imports==1.10.0] + + - repo: https://github.com/asottile/setup-cfg-fmt + rev: v1.15.0 + hooks: + - id: setup-cfg-fmt diff --git a/README.md b/README.md index 3cd8ac1a1..7f8bb6619 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Development Clone the repo and install dependencies: - git clone + git clone cd netatmo-api-python pipenv install --dev diff --git a/setup.cfg b/setup.cfg index 04bc68dc3..8596c9479 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,14 +57,12 @@ ignore_errors = False ignore_errors = False [isort] -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +use_parentheses = True forced_separate = tests combine_as_imports = true -line_length=88 -not_skip = __init__.py +line_length = 88 skip_glob = venv -known_first_party = pyatmo +known_third_party = freezegun,oauthlib,pytest,requests,requests_oauthlib,setuptools,tests diff --git a/src/pyatmo/__main__.py b/src/pyatmo/__main__.py index 1ba1a12e0..f8c05d41a 100644 --- a/src/pyatmo/__main__.py +++ b/src/pyatmo/__main__.py @@ -29,7 +29,7 @@ def main(): except KeyError: sys.stderr.write( "No credentials passed to pyatmo.py (client_id, client_secret, " - "username, password)\n" + "username, password)\n", ) sys.exit(1) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 7814db4dd..673e7b964 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -4,8 +4,8 @@ from typing import Any, Callable, Dict, Optional, Tuple, Union import requests -from oauthlib.oauth2 import LegacyApplicationClient, TokenExpiredError # type: ignore -from requests_oauthlib import OAuth2Session # type: ignore +from oauthlib.oauth2 import LegacyApplicationClient, TokenExpiredError +from requests_oauthlib import OAuth2Session from pyatmo.exceptions import ApiError from pyatmo.helpers import _BASE_URL, ERRORS @@ -137,7 +137,9 @@ def query(url: str, params: Dict, timeout: int, retries: int) -> Any: try: if json_params: rsp = self._oauth.post( - url=url, json=json_params, timeout=timeout + url=url, + json=json_params, + timeout=timeout, ) else: @@ -171,14 +173,14 @@ def query(url: str, params: Dict, timeout: int, retries: int) -> Any: f"{ERRORS.get(resp.status_code, '')} - " f"{resp.json()['error']['message']} " f"({resp.json()['error']['code']}) " - f"when accessing '{url}'" + f"when accessing '{url}'", ) except JSONDecodeError as exc: raise ApiError( f"{resp.status_code} - " f"{ERRORS.get(resp.status_code, '')} - " - f"when accessing '{url}'" + f"when accessing '{url}'", ) from exc try: @@ -197,7 +199,9 @@ def get_authorization_url(self, state: Optional[str] = None) -> Tuple[str, str]: return self._oauth.authorization_url(AUTH_URL, state) def request_token( - self, authorization_response: Optional[str] = None, code: Optional[str] = None + self, + authorization_response: Optional[str] = None, + code: Optional[str] = None, ) -> Dict[str, str]: """ Generic method for fetching a Netatmo access token. @@ -258,7 +262,7 @@ def __init__( super().__init__(client_id=client_id, client_secret=client_secret, scope=scope) self._oauth = OAuth2Session( - client=LegacyApplicationClient(client_id=self.client_id) + client=LegacyApplicationClient(client_id=self.client_id), ) self._oauth.fetch_token( token_url=AUTH_REQ, diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 290b6292c..1ef0b5e4b 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -177,7 +177,7 @@ def check_url(url: str) -> Optional[str]: temp_local_url = check_url(vpn_url) if temp_local_url: self.cameras[home_id][camera_id]["local_url"] = check_url( - temp_local_url + temp_local_url, ) def get_light_state(self, camera_id: str) -> Optional[str]: @@ -239,7 +239,9 @@ def get_person_id(self, name: str) -> Optional[str]: return None def get_camera_picture( - self, image_id: str, key: str + self, + image_id: str, + key: str, ) -> Tuple[bytes, Optional[str]]: """Download a specific image (of an event or user face) from the camera.""" post_params = { @@ -264,7 +266,10 @@ def get_profile_image(self, name: str) -> Tuple[Optional[bytes], Optional[str]]: return None, None def update_events( - self, home_id: str, event_id: str = None, device_type: str = None + self, + home_id: str, + event_id: str = None, + device_type: str = None, ) -> None: """Update the list of events.""" # Either event_id or device_type must be given @@ -332,7 +337,10 @@ def get_event_id(data: Dict): ] def person_seen_by_camera( - self, name: str, camera_id: str, exclude: int = 0 + self, + name: str, + camera_id: str, + exclude: int = 0, ) -> bool: """Evaluate if a specific person has been seen.""" # Check in the last event is someone known has been seen @@ -468,7 +476,7 @@ def human_detected(self, camera_id: str, offset: int = 0) -> bool: if self.outdoor_last_event[camera_id]["video_status"] == "recording": for event in self.outdoor_last_event[camera_id]["event_list"]: if event["type"] == "human" and event["time"] + offset > int( - time.time() + time.time(), ): return True @@ -479,7 +487,7 @@ def animal_detected(self, camera_id: str, offset: int = 0) -> bool: if self.outdoor_last_event[camera_id]["video_status"] == "recording": for event in self.outdoor_last_event[camera_id]["event_list"]: if event["type"] == "animal" and event["time"] + offset > int( - time.time() + time.time(), ): return True @@ -490,14 +498,17 @@ def car_detected(self, camera_id: str, offset: int = 0) -> bool: if self.outdoor_last_event[camera_id]["video_status"] == "recording": for event in self.outdoor_last_event[camera_id]["event_list"]: if event["type"] == "vehicle" and event["time"] + offset > int( - time.time() + time.time(), ): return True return False def module_motion_detected( - self, module_id: str, camera_id: str, exclude: int = 0 + self, + module_id: str, + camera_id: str, + exclude: int = 0, ) -> bool: """Evaluate if movement has been detected.""" if exclude: diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 3af9b9ba8..108c940de 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -228,7 +228,10 @@ def boiler_status(self, module_id: str) -> Optional[bool]: return self.get_thermostat(module_id).get("boiler_status") def set_thermmode( - self, mode: str, end_time: int = None, schedule_id: str = None + self, + mode: str, + end_time: int = None, + schedule_id: str = None, ) -> Optional[str]: post_params = { "home_id": self.home_id, @@ -243,7 +246,11 @@ def set_thermmode( return self.auth.post_request(url=_SETTHERMMODE_REQ, params=post_params) def set_room_thermpoint( - self, room_id: str, mode: str, temp: float = None, end_time: int = None + self, + room_id: str, + mode: str, + temp: float = None, + end_time: int = None, ) -> Optional[str]: post_params = { "home_id": self.home_id, diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index b25561f94..9b8329ff9 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -37,7 +37,7 @@ def __init__(self, auth: NetatmoOAuth2, url_req: str = None) -> None: except KeyError as exc: LOG.debug("No in response %s", resp) raise NoDevice( - "No weather station data returned by Netatmo server" + "No weather station data returned by Netatmo server", ) from exc if not self.raw_data: @@ -128,7 +128,7 @@ def get_monitored_conditions(self, module_id: str) -> List: if condition == "Wind": # the Wind meter actually exposes the following conditions conditions.extend( - ["WindAngle", "WindStrength", "GustAngle", "GustStrength"] + ["WindAngle", "WindStrength", "GustAngle", "GustStrength"], ) elif condition == "Rain": @@ -257,7 +257,10 @@ def get_data( return self.auth.post_request(url=_GETMEASURE_REQ, params=post_params) def get_min_max_t_h( - self, station_id: str, module_id: str = None, frame: str = "last24" + self, + station_id: str, + module_id: str = None, + frame: str = "last24", ) -> Optional[Tuple[float, float, float, float]]: """Return minimum and maximum temperature and humidity over the given timeframe. diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index 7263a8baf..a21aa850c 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -132,7 +132,11 @@ def test_camera_data_persons_at_home(camera_home_data, home_id, expected): ], ) def test_camera_data_person_seen_by_camera( - camera_home_data, name, cid, exclude, expected + camera_home_data, + name, + cid, + exclude, + expected, ): assert ( camera_home_data.person_seen_by_camera(name, cid, exclude=exclude) is expected @@ -192,7 +196,12 @@ def test_camera_data_get_person_id(camera_home_data, name, expected): ], ) def test_camera_data_set_persons_away( - camera_home_data, requests_mock, home_id, person_id, json_fixture, expected + camera_home_data, + requests_mock, + home_id, + person_id, + json_fixture, + expected, ): with open("fixtures/%s" % json_fixture) as json_file: json_fixture = json.load(json_file) @@ -225,7 +234,12 @@ def test_camera_data_set_persons_away( ], ) def test_camera_data_set_persons_home( - camera_home_data, requests_mock, home_id, person_ids, json_fixture, expected + camera_home_data, + requests_mock, + home_id, + person_ids, + json_fixture, + expected, ): with open("fixtures/%s" % json_fixture) as json_file: json_fixture = json.load(json_file) @@ -247,7 +261,11 @@ def test_camera_data_set_persons_home( ], ) def test_camera_data_someone_known_seen( - camera_home_data, camera_id, exclude, expected, expectation + camera_home_data, + camera_id, + exclude, + expected, + expectation, ): with expectation: assert camera_home_data.someone_known_seen(camera_id, exclude) == expected @@ -263,7 +281,11 @@ def test_camera_data_someone_known_seen( ], ) def test_camera_data_someone_unknown_seen( - camera_home_data, camera_id, exclude, expected, expectation + camera_home_data, + camera_id, + exclude, + expected, + expectation, ): with expectation: assert camera_home_data.someone_unknown_seen(camera_id, exclude) == expected @@ -280,7 +302,11 @@ def test_camera_data_someone_unknown_seen( ], ) def test_camera_data_motion_detected( - camera_home_data, camera_id, exclude, expected, expectation + camera_home_data, + camera_id, + exclude, + expected, + expectation, ): with expectation: assert camera_home_data.motion_detected(camera_id, exclude) == expected @@ -426,7 +452,12 @@ def test_camera_data_get_profile_image(camera_home_data, requests_mock): ], ) def test_camera_data_update_events( - camera_home_data, requests_mock, home_id, event_id, device_type, exception + camera_home_data, + requests_mock, + home_id, + event_id, + device_type, + exception, ): with open("fixtures/camera_data_events_until.json") as fixture_file: json_fixture = json.load(fixture_file) @@ -440,7 +471,9 @@ def test_camera_data_update_events( before = camera_home_data.last_event.copy() assert ( camera_home_data.update_events( - home_id=home_id, event_id=event_id, device_type=device_type + home_id=home_id, + event_id=event_id, + device_type=device_type, ) is None ) diff --git a/tests/test_pyatmo_homecoach.py b/tests/test_pyatmo_homecoach.py index 9179f207a..5411df567 100644 --- a/tests/test_pyatmo_homecoach.py +++ b/tests/test_pyatmo_homecoach.py @@ -19,7 +19,7 @@ def test_home_coach_data(home_coach_data): "NoValidStation", None, marks=pytest.mark.xfail( - reason="Invalid station names are not handled yet." + reason="Invalid station names are not handled yet.", ), ), ], @@ -39,14 +39,14 @@ def test_home_coach_data_get_module_names(home_coach_data, station_id, expected) "station_name": "Bedroom", "module_name": "Bedroom", "id": "12:34:56:26:69:0c", - } + }, }, ), pytest.param( "NoValidStation", None, marks=pytest.mark.xfail( - reason="Invalid station names are not handled yet." + reason="Invalid station names are not handled yet.", ), ), ], diff --git a/tests/test_pyatmo_publicdata.py b/tests/test_pyatmo_publicdata.py index 755ecb53f..dc96a40e8 100644 --- a/tests/test_pyatmo_publicdata.py +++ b/tests/test_pyatmo_publicdata.py @@ -25,7 +25,12 @@ def test_public_data(auth, requests_mock): assert public_data.status == "ok" public_data = pyatmo.PublicData( - auth, LAT_NE, LON_NE, LAT_SW, LON_SW, required_data_type="temperature,rain_live" + auth, + LAT_NE, + LON_NE, + LAT_SW, + LON_SW, + required_data_type="temperature,rain_live", ) assert public_data.status == "ok" diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index b71444bd6..6c28db9d8 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -128,7 +128,11 @@ def test_home_data_get_selected_schedule(home_data): ], ) def test_home_data_switch_home_schedule( - home_data, requests_mock, t_home_id, t_sched_id, expected + home_data, + requests_mock, + t_home_id, + t_sched_id, + expected, ): with open("fixtures/status_ok.json") as json_file: json_fixture = json.load(json_file) @@ -407,7 +411,9 @@ def test_home_status_set_thermmode( headers={"content-type": "application/json"}, ) res = home_status.set_thermmode( - mode=mode, end_time=end_time, schedule_id=schedule_id + mode=mode, + end_time=end_time, + schedule_id=schedule_id, ) if "error" in res: assert expected in res["error"]["message"] @@ -475,7 +481,10 @@ def test_home_status_set_room_thermpoint( ) assert ( home_status.set_room_thermpoint( - room_id=room_id, mode=mode, temp=temp, end_time=end_time + room_id=room_id, + mode=mode, + temp=temp, + end_time=end_time, )["status"] == expected ) @@ -543,7 +552,9 @@ def test_home_status_set_room_thermpoint_error( def test_home_status_error_disconnected( - auth, requests_mock, home_id="91763b24c43d3e344f424e8b" + auth, + requests_mock, + home_id="91763b24c43d3e344f424e8b", ): with open("fixtures/home_status_error_disconnected.json") as json_file: json_fixture = json.load(json_file) diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 8c615596b..bdded0a96 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -67,7 +67,7 @@ def test_weather_station_data_no_data(auth, requests_mock): "NoValidStation", None, marks=pytest.mark.xfail( - reason="Invalid station names are not handled yet." + reason="Invalid station names are not handled yet.", ), ), ], @@ -125,14 +125,14 @@ def test_weather_station_get_module_names(weather_station_data, station_id, expe "id": "12:34:56:1d:68:2e", "module_name": "Basisstation", "station_name": "NAMain", - } + }, }, ), pytest.param( "NoValidStation", None, marks=pytest.mark.xfail( - reason="Invalid station names are not handled yet." + reason="Invalid station names are not handled yet.", ), ), ], @@ -251,7 +251,9 @@ def test_weather_station_get_module(weather_station_data, mid, expected): ], ) def test_weather_station_get_monitored_conditions( - weather_station_data, module_id, expected + weather_station_data, + module_id, + expected, ): assert sorted(weather_station_data.get_monitored_conditions(module_id)) == expected @@ -305,7 +307,10 @@ def test_weather_station_get_monitored_conditions( ], ) def test_weather_station_get_last_data( - weather_station_data, station_id, exclude, expected + weather_station_data, + station_id, + exclude, + expected, ): mod = weather_station_data.get_last_data(station_id, exclude=exclude) if mod: @@ -344,7 +349,10 @@ def test_weather_station_get_last_data( ], ) def test_weather_station_check_not_updated( - weather_station_data, station_id, delay, expected + weather_station_data, + station_id, + delay, + expected, ): mod = weather_station_data.check_not_updated(station_id, delay) assert sorted(mod) == expected @@ -374,7 +382,10 @@ def test_weather_station_check_not_updated( ], ) def test_weather_station_check_updated( - weather_station_data, station_id, delay, expected + weather_station_data, + station_id, + delay, + expected, ): mod = weather_station_data.check_updated(station_id, delay) if mod: @@ -385,10 +396,16 @@ def test_weather_station_check_updated( @freeze_time("2019-06-11") @pytest.mark.parametrize( - "device_id, scale, module_type, expected", [("MyStation", "scale", "type", [28.1])] + "device_id, scale, module_type, expected", + [("MyStation", "scale", "type", [28.1])], ) def test_weather_station_get_data( - weather_station_data, requests_mock, device_id, scale, module_type, expected + weather_station_data, + requests_mock, + device_id, + scale, + module_type, + expected, ): with open("fixtures/weatherstation_measure.json") as json_file: json_fixture = json.load(json_file) @@ -448,7 +465,10 @@ def test_weather_station_get_last_data_measurements(weather_station_data): ], ) def test_weather_station_get_last_data_bug_97( - weather_station_data, station_id, exclude, expected + weather_station_data, + station_id, + exclude, + expected, ): mod = weather_station_data.get_last_data(station_id, exclude) if mod: diff --git a/usage.md b/usage.md index 540179152..92d8a2178 100644 --- a/usage.md +++ b/usage.md @@ -130,7 +130,7 @@ The results are Python data structures, mostly dictionaries as they mirror easil _BASE_URL and _*_REQ : Various URL to access Netatmo web services. They are documented in http://dev.netatmo.com/doc/ They should not be changed unless Netatmo API changes. -``` +``` @@ -439,4 +439,3 @@ Methods : * **to_time_string** (timestamp) : Convert a Netatmo time stamp to a readable date/time format. * **to_epoch**( dateString) : Convert a date string (form YYYY-MM-DD_HH:MM:SS) to timestamp * **today_stamps**() : Return a couple of epoch time (start, end) for the current day - From e9920e461ea0eafa6084343d8fa47d30c559b381 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Mon, 12 Oct 2020 15:38:33 +0200 Subject: [PATCH 287/333] Github Actions: Add black output when failing Signed-off-by: Hugo Dupras --- .github/workflows/pythonpackage.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 6df166195..8f6df2b50 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -16,7 +16,7 @@ jobs: strategy: max-parallel: 1 matrix: - python-version: [3.6] + python-version: [3.7] steps: - uses: actions/checkout@v2.3.3 @@ -27,14 +27,14 @@ jobs: - name: Check with black run: | pip install black - black --check src/pyatmo/ tests/ setup.py + black --check --diff src/pyatmo/ tests/ setup.py linter: runs-on: ubuntu-latest strategy: - max-parallel: 1 + max-parallel: 4 matrix: - python-version: [3.6] + python-version: [ 3.7, 3.8 ] steps: - uses: actions/checkout@v2.3.3 @@ -56,7 +56,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.7, 3.8] steps: - uses: actions/checkout@v2.3.3 From 3600e72d7eefd9529a40da9de97ff1544f71a1b1 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Mon, 12 Oct 2020 15:38:52 +0200 Subject: [PATCH 288/333] Upgrade tools and deps Signed-off-by: Hugo Dupras --- Pipfile | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Pipfile b/Pipfile index 2b99328d2..1d438ffff 100644 --- a/Pipfile +++ b/Pipfile @@ -4,26 +4,26 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -twine = "*" -pytest = "*" black = "*" -mypy = "*" +bleach = ">=3.1.4" +docutils = "*" flake8 = "*" +freezegun = "*" +isort = "*" +mypy = "*" +pre-commit = "*" pylint = "*" -docutils = "*" -bleach = ">=3.1.4" -pytest-mock = "*" +pytest = "*" pytest-cov = "*" +pytest-mock = "*" requests-mock = "*" -freezegun = "*" -isort = "*" tox = "*" -pre-commit = "*" +twine = "*" [packages] -pyatmo = {editable = true,path = "."} requests = "*" requests-oauthlib = "*" +pyatmo = {editable = true,path = "."} [requires] python_version = "3.7" From 5f283189c1e818888c0f425d8a4e0e3f623e0325 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Tue, 20 Oct 2020 17:32:49 +0200 Subject: [PATCH 289/333] Add usage version.py for version management Signed-off-by: Hugo Dupras --- .github/dependabot.yml | 2 +- setup.cfg | 2 +- version.py | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 version.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c1c40362c..02f2ab716 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,7 +10,7 @@ updates: schedule: # Check for updates to GitHub Actions every weekday interval: "daily" - + - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: diff --git a/setup.cfg b/setup.cfg index 8596c9479..650d5f94d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyatmo -version = 4.1.0 +version = attr:version.__version__ description = Simple API to access Netatmo weather station data from any Python 3 script. Design for Home-Assitant (but not only) long_description = file: README.md long_description_content_type = text/markdown diff --git a/version.py b/version.py new file mode 100644 index 000000000..b3c04842e --- /dev/null +++ b/version.py @@ -0,0 +1,4 @@ +MAJOR_VERSION = 4 +MINOR_VERSION = 1 +PATCH_VERSION = "0" +__version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}" From bd06efa2af6551fdb2f192d418edca4fb9ff0e37 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Tue, 20 Oct 2020 17:47:34 +0200 Subject: [PATCH 290/333] Move version.py to be available at build time Signed-off-by: Hugo Dupras --- setup.cfg | 2 ++ version.py => src/version.py | 0 2 files changed, 2 insertions(+) rename version.py => src/version.py (100%) diff --git a/setup.cfg b/setup.cfg index 650d5f94d..545f6e5af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,9 +18,11 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy + Topic :: Home Automation" [options] packages = find: +py_modules = version install_requires = oauthlib requests diff --git a/version.py b/src/version.py similarity index 100% rename from version.py rename to src/version.py From d91df9f59028b442e3316424d6312996d4959e9e Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Tue, 20 Oct 2020 20:32:17 +0200 Subject: [PATCH 291/333] Simplify with defaultdict (#135) defaultdict have better performance that checking a key in a dict Signed-off-by: Hugo Dupras --- src/pyatmo/camera.py | 61 ++++++++++++++++------------------------ src/pyatmo/thermostat.py | 46 ++++++++++-------------------- 2 files changed, 40 insertions(+), 67 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 1ef0b5e4b..d0d0196cb 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -1,6 +1,7 @@ import imghdr import time -from typing import Dict, List, Optional, Tuple +from collections import defaultdict +from typing import Any, Dict, List, Optional, Tuple from requests.exceptions import ReadTimeout @@ -48,53 +49,39 @@ def __init__(self, auth: NetatmoOAuth2, size: int = 30) -> None: self.homes: Dict = {d["id"]: d for d in self.raw_data} self.persons: Dict = {} - self.events: Dict = {} - self.outdoor_events: Dict = {} - self.cameras: Dict = {} - self.smokedetectors: Dict = {} + self.events: Dict = defaultdict(dict) + self.outdoor_events: Dict = defaultdict(dict) + self.cameras: Dict = defaultdict(dict) + self.smokedetectors: Dict = defaultdict(dict) self.modules: Dict = {} self.last_event: Dict = {} self.outdoor_last_event: Dict = {} - self.types: Dict = {} + self.types: Dict = defaultdict(dict) for item in self.raw_data: - home_id: str = item.get("id") - home_name: str = item.get("name") + home_id: str = item.get("id", "") + home_name: str = item.get("name", "") if not home_name: home_name = "Unknown" self.homes[home_id]["name"] = home_name - if home_id not in self.cameras: - self.cameras[home_id] = {} - - if home_id not in self.smokedetectors: - self.smokedetectors[home_id] = {} - - if home_id not in self.types: - self.types[home_id] = {} - for person in item["persons"]: self.persons[person["id"]] = person - if "events" in item: - for event in item["events"]: - if event["type"] == "outdoor": - if event["camera_id"] not in self.outdoor_events: - self.outdoor_events[event["camera_id"]] = {} - self.outdoor_events[event["camera_id"]][event["time"]] = event + for event in item.get("events", []): + if event["type"] == "outdoor": + self.outdoor_events[event["camera_id"]][event["time"]] = event - else: - if event["camera_id"] not in self.events: - self.events[event["camera_id"]] = {} - self.events[event["camera_id"]][event["time"]] = event + else: + self.events[event["camera_id"]][event["time"]] = event for camera in item["cameras"]: self.cameras[home_id][camera["id"]] = camera self.cameras[home_id][camera["id"]]["home_id"] = home_id - if camera["type"] == "NACamera" and "modules" in camera: - for module in camera["modules"]: + if camera["type"] == "NACamera": + for module in camera.get("modules", []): self.modules[module["id"]] = module self.modules[module["id"]]["cam_id"] = camera["id"] @@ -306,24 +293,26 @@ def get_event_id(data: Dict): "event_id": event_id, } + event_list = [] + resp: Optional[Dict[str, Any]] = None try: resp = self.auth.post_request(url=_GETEVENTSUNTIL_REQ, params=post_params) - event_list = resp["body"]["events_list"] + if resp is not None: + event_list = resp["body"]["events_list"] except ApiError: pass except KeyError: - LOG.debug("event_list response: %s", resp) - LOG.debug("event_list body: %s", resp["body"]) + if resp is not None: + LOG.debug("event_list response: %s", resp) + LOG.debug("event_list body: %s", dict(resp)["body"]) + else: + LOG.debug("No resp received") for event in event_list: if event["type"] == "outdoor": - if event["camera_id"] not in self.outdoor_events: - self.outdoor_events[event["camera_id"]] = {} self.outdoor_events[event["camera_id"]][event["time"]] = event else: - if event["camera_id"] not in self.events: - self.events[event["camera_id"]] = {} self.events[event["camera_id"]][event["time"]] = event for camera in self.events: diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 108c940de..7187fbc8d 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -1,4 +1,5 @@ import logging +from collections import defaultdict from typing import Any, Dict, Optional from .auth import NetatmoOAuth2 @@ -40,11 +41,11 @@ def __init__(self, auth: NetatmoOAuth2) -> None: self.homes: Dict = {d["id"]: d for d in self.raw_data} - self.modules: Dict = {} - self.rooms: Dict = {} - self.schedules: Dict = {} - self.zones: Dict = {} - self.setpoint_duration: Dict = {} + self.modules: Dict = defaultdict(dict) + self.rooms: Dict = defaultdict(dict) + self.schedules: Dict = defaultdict(dict) + self.zones: Dict = defaultdict(dict) + self.setpoint_duration: Dict = defaultdict(dict) for item in self.raw_data: home_id = item.get("id") @@ -55,44 +56,27 @@ def __init__(self, auth: NetatmoOAuth2) -> None: self.homes[home_id]["name"] = home_name if "modules" in item: - if home_id not in self.modules: - self.modules[home_id] = {} for module in item["modules"]: self.modules[home_id][module["id"]] = module - if home_id not in self.rooms: - self.rooms[home_id] = {} - - if home_id not in self.schedules: - self.schedules[home_id] = {} - - if home_id not in self.zones: - self.zones[home_id] = {} - - if home_id not in self.setpoint_duration: - self.setpoint_duration[home_id] = {} - if "therm_setpoint_default_duration" in item: self.setpoint_duration[home_id] = item[ "therm_setpoint_default_duration" ] - if "rooms" in item: - for room in item["rooms"]: - self.rooms[home_id][room["id"]] = room + for room in item.get("rooms", []): + self.rooms[home_id][room["id"]] = room - if "therm_schedules" in item: - for schedule in item["therm_schedules"]: - self.schedules[home_id][schedule["id"]] = schedule + for schedule in item.get("therm_schedules", []): + schedule_id = schedule["id"] + self.schedules[home_id][schedule_id] = schedule - for schedule in item["therm_schedules"]: - schedule_id = schedule["id"] - if schedule_id not in self.zones[home_id]: - self.zones[home_id][schedule_id] = {} + if schedule_id not in self.zones[home_id]: + self.zones[home_id][schedule_id] = {} - for zone in schedule["zones"]: - self.zones[home_id][schedule_id][zone["id"]] = zone + for zone in schedule["zones"]: + self.zones[home_id][schedule_id][zone["id"]] = zone def _get_selected_schedule(self, home_id: str) -> Dict: """Get the selected schedule for a given home ID.""" From dde0748d403e56c56bb099ca4c5dec67f4521cd1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 21 Oct 2020 10:07:17 +0200 Subject: [PATCH 292/333] Pin requests to latest version 2.24.0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 545f6e5af..5b4a38939 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ packages = find: py_modules = version install_requires = oauthlib - requests + requests==2.24.0 requests_oauthlib python_requires = >=3.7 package_dir = =src From 8cac45c646599365c8af6c13eb6dbad47467ccee Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 21 Oct 2020 10:07:20 +0200 Subject: [PATCH 293/333] Pin requests_oauthlib to latest version 1.3.0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5b4a38939..feb9185ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,7 +26,7 @@ py_modules = version install_requires = oauthlib requests==2.24.0 - requests_oauthlib + requests_oauthlib==1.3.0 python_requires = >=3.7 package_dir = =src From 784d8d225e3e157037863ef0ef401292f657151d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 21 Oct 2020 10:07:14 +0200 Subject: [PATCH 294/333] Pin oauthlib to latest version 3.1.0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index feb9185ae..5c666dd01 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,7 @@ classifiers = packages = find: py_modules = version install_requires = - oauthlib + oauthlib==3.1.0 requests==2.24.0 requests_oauthlib==1.3.0 python_requires = >=3.7 From 9ec04896de0762ef63f5cd6864ed3c6be473f0b3 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Wed, 21 Oct 2020 10:31:53 +0200 Subject: [PATCH 295/333] Minor fix for setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5c666dd01..a74a57f2c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy - Topic :: Home Automation" + Topic :: Home Automation [options] packages = find: From 817471ef186225be558a1201992552fb7aec7da4 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Wed, 21 Oct 2020 10:31:53 +0200 Subject: [PATCH 296/333] Minor fix for setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index a74a57f2c..4fb919cb3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] name = pyatmo version = attr:version.__version__ -description = Simple API to access Netatmo weather station data from any Python 3 script. Design for Home-Assitant (but not only) +description = Simple API to access Netatmo weather station data from any Python 3 script. Designed for Home-Assitant (but not only) long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/jabesq/netatmo-api-python From 93c42bc91f9f49c99487699fed96d88f0e4677da Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 14 Oct 2020 11:20:43 +0200 Subject: [PATCH 297/333] Update action --- .github/workflows/pythonpackage.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 8f6df2b50..77151c343 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -24,9 +24,12 @@ jobs: uses: actions/setup-python@v2.1.4 with: python-version: ${{ matrix.python-version }} - - name: Check with black + - name: Install dependencies run: | + python -m pip install --upgrade pip pip install black + - name: Check with black + run: | black --check --diff src/pyatmo/ tests/ setup.py linter: @@ -34,7 +37,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [ 3.7, 3.8 ] + python-version: [ 3.8 ] steps: - uses: actions/checkout@v2.3.3 @@ -42,9 +45,12 @@ jobs: uses: actions/setup-python@v2.1.4 with: python-version: ${{ matrix.python-version }} - - name: Lint with flake8 + - name: Install dependencies run: | + python -m pip install --upgrade pip pip install flake8 + - name: Lint with flake8 + run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide @@ -56,7 +62,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.7, 3.8] + python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2.3.3 From 6a00c1a74ca32c54530a83e2532d939a8cf89e22 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 14 Oct 2020 11:31:43 +0200 Subject: [PATCH 298/333] Add publish to test pypi --- .github/workflows/publish-to-test-pypi.yml | 38 ++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/publish-to-test-pypi.yml diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml new file mode 100644 index 000000000..9d55e44e5 --- /dev/null +++ b/.github/workflows/publish-to-test-pypi.yml @@ -0,0 +1,38 @@ +name: Publish Python 🐍 distributions đŸ“Ļ to PyPI and TestPyPI + +on: + push: + tags: + - '*' + +jobs: + build-n-publish: + name: Build and publish Python 🐍 distributions đŸ“Ļ to PyPI and TestPyPI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + - name: Build a binary wheel and a source tarball + run: >- + python setup.py + --source + --binary + --out-dir dist/ + . + - name: Publish distribution đŸ“Ļ to Test PyPI + uses: pypa/gh-action-pypi-publish@master + with: + password: ${{ secrets.test_pypi_password }} + repository_url: https://test.pypi.org/legacy/ + # - name: Publish distribution đŸ“Ļ to PyPI + # if: startsWith(github.ref, 'refs/tags') + # uses: pypa/gh-action-pypi-publish@master + # with: + # password: ${{ secrets.pypi_password }} From 230e780cf2f83196593ae146c9985229c7a9fa23 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 14 Oct 2020 11:42:10 +0200 Subject: [PATCH 299/333] Try on create tags --- .github/workflows/publish-to-test-pypi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index 9d55e44e5..902cba358 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -1,7 +1,7 @@ name: Publish Python 🐍 distributions đŸ“Ļ to PyPI and TestPyPI on: - push: + create: tags: - '*' From d22f378413c8c68df575c75caf8e7b4ea88a73dd Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 14 Oct 2020 11:52:55 +0200 Subject: [PATCH 300/333] Fix setup.py cmd --- .github/workflows/publish-to-test-pypi.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index 902cba358..c1967ab69 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -21,15 +21,12 @@ jobs: pip install setuptools wheel - name: Build a binary wheel and a source tarball run: >- - python setup.py - --source - --binary - --out-dir dist/ - . + python setup.py sdist bdist_wheel - name: Publish distribution đŸ“Ļ to Test PyPI uses: pypa/gh-action-pypi-publish@master with: - password: ${{ secrets.test_pypi_password }} + username: "__token__" + password: ${{ secrets.pypi_test_token }} repository_url: https://test.pypi.org/legacy/ # - name: Publish distribution đŸ“Ļ to PyPI # if: startsWith(github.ref, 'refs/tags') From c9cc221c67240b330a9f6ffe5de3197bd1445718 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 14 Oct 2020 13:02:01 +0200 Subject: [PATCH 301/333] Extract version into variable --- version.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 version.py diff --git a/version.py b/version.py new file mode 100644 index 000000000..3b24e4a33 --- /dev/null +++ b/version.py @@ -0,0 +1,4 @@ +MAJOR_VERSION = 4 +MINOR_VERSION = 1 +PATCH_VERSION = "0.dev0" +__version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}" From a86be55139482e725fdb879f7892b8dd029c4d2f Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 14 Oct 2020 13:04:50 +0200 Subject: [PATCH 302/333] Add publish to test pypi workflow --- .github/workflows/publish-to-test-pypi.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index c1967ab69..b3f108679 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -28,8 +28,11 @@ jobs: username: "__token__" password: ${{ secrets.pypi_test_token }} repository_url: https://test.pypi.org/legacy/ +<<<<<<< HEAD # - name: Publish distribution đŸ“Ļ to PyPI # if: startsWith(github.ref, 'refs/tags') # uses: pypa/gh-action-pypi-publish@master # with: # password: ${{ secrets.pypi_password }} +======= +>>>>>>> Add publish to test pypi workflow From b4c464a7e32ef2b09e5334248ba243d4f3f593ca Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 14 Oct 2020 14:14:18 +0200 Subject: [PATCH 303/333] Add workflow for publishing to pypi --- .github/workflows/publish-to-pypi.yml | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/publish-to-pypi.yml diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml new file mode 100644 index 000000000..874f51017 --- /dev/null +++ b/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,29 @@ +name: Publish Python 🐍 distributions đŸ“Ļ to PyPI and TestPyPI + +on: + create: + tags: + - v[0-9]+.[0-9]+.[0-9]+ + +jobs: + build-n-publish: + name: Build and publish Python 🐍 distributions đŸ“Ļ to PyPI and TestPyPI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + - name: Build a binary wheel and a source tarball + run: >- + python setup.py sdist bdist_wheel + - name: Publish distribution đŸ“Ļ to PyPI + if: startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@master + with: + password: ${{ secrets.pypi_prod_token }} \ No newline at end of file From 791dd86d4f9e027e6adad51cf7e798a97ec3a4cf Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 14 Oct 2020 14:14:43 +0200 Subject: [PATCH 304/333] Fix names --- .github/workflows/publish-to-pypi.yml | 2 +- .github/workflows/publish-to-test-pypi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 874f51017..42c286bd3 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -1,4 +1,4 @@ -name: Publish Python 🐍 distributions đŸ“Ļ to PyPI and TestPyPI +name: Publish Python 🐍 distributions đŸ“Ļ to PyPI on: create: diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index b3f108679..1452839ba 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -1,4 +1,4 @@ -name: Publish Python 🐍 distributions đŸ“Ļ to PyPI and TestPyPI +name: Publish Python 🐍 distributions đŸ“Ļ to TestPyPI on: create: From 463a3c26109b73aa6f0f7d9db35ab9a9e49c6c5e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 14 Oct 2020 14:20:42 +0200 Subject: [PATCH 305/333] Fix names --- .github/workflows/publish-to-pypi.yml | 2 +- .github/workflows/publish-to-test-pypi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 42c286bd3..4c6b8dddf 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -7,7 +7,7 @@ on: jobs: build-n-publish: - name: Build and publish Python 🐍 distributions đŸ“Ļ to PyPI and TestPyPI + name: Build and publish Python 🐍 distributions đŸ“Ļ to PyPI runs-on: ubuntu-latest steps: - uses: actions/checkout@master diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index 1452839ba..bc8d351be 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -7,7 +7,7 @@ on: jobs: build-n-publish: - name: Build and publish Python 🐍 distributions đŸ“Ļ to PyPI and TestPyPI + name: Build and publish Python 🐍 distributions đŸ“Ļ to TestPyPI runs-on: ubuntu-latest steps: - uses: actions/checkout@master From 7f080e3117ac3546e9fd15804b00018ce0c690bd Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 14 Oct 2020 14:21:47 +0200 Subject: [PATCH 306/333] Reduce name length --- .github/workflows/publish-to-pypi.yml | 4 ++-- .github/workflows/publish-to-test-pypi.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 4c6b8dddf..2e0c7e15a 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -1,4 +1,4 @@ -name: Publish Python 🐍 distributions đŸ“Ļ to PyPI +name: Publish đŸ“Ļ to PyPI on: create: @@ -7,7 +7,7 @@ on: jobs: build-n-publish: - name: Build and publish Python 🐍 distributions đŸ“Ļ to PyPI + name: Build and publish đŸ“Ļ to PyPI runs-on: ubuntu-latest steps: - uses: actions/checkout@master diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index bc8d351be..3607787bd 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -1,4 +1,4 @@ -name: Publish Python 🐍 distributions đŸ“Ļ to TestPyPI +name: Publish đŸ“Ļ to TestPyPI on: create: @@ -7,7 +7,7 @@ on: jobs: build-n-publish: - name: Build and publish Python 🐍 distributions đŸ“Ļ to TestPyPI + name: Build and publish đŸ“Ļ to TestPyPI runs-on: ubuntu-latest steps: - uses: actions/checkout@master From 21f77d91d0201baba1098fc5e1511554973e500c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 14 Oct 2020 14:22:21 +0200 Subject: [PATCH 307/333] Update workflows --- .github/workflows/publish-to-pypi.yml | 2 +- .github/workflows/publish-to-test-pypi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 2e0c7e15a..df2cd812c 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -22,7 +22,7 @@ jobs: - name: Build a binary wheel and a source tarball run: >- python setup.py sdist bdist_wheel - - name: Publish distribution đŸ“Ļ to PyPI + - name: Publish đŸ“Ļ to PyPI if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@master with: diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index 3607787bd..1e000c25e 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -22,7 +22,7 @@ jobs: - name: Build a binary wheel and a source tarball run: >- python setup.py sdist bdist_wheel - - name: Publish distribution đŸ“Ļ to Test PyPI + - name: Publish đŸ“Ļ to Test PyPI uses: pypa/gh-action-pypi-publish@master with: username: "__token__" From 160c45ec45d3a4c44e6f396159692cabf83f8ffe Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 14 Oct 2020 14:35:45 +0200 Subject: [PATCH 308/333] Update workflows --- .github/workflows/publish-to-pypi.yml | 42 +++++++++++----------- .github/workflows/publish-to-test-pypi.yml | 22 +++++++++++- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index df2cd812c..34773fa9d 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -1,29 +1,31 @@ name: Publish đŸ“Ļ to PyPI on: - create: - tags: - - v[0-9]+.[0-9]+.[0-9]+ + push: + branches: + - master + tags-ignore: + - "*.*b*" jobs: build-n-publish: name: Build and publish đŸ“Ļ to PyPI runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - name: Set up Python 3.7 - uses: actions/setup-python@v1 - with: - python-version: 3.7 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel - - name: Build a binary wheel and a source tarball - run: >- - python setup.py sdist bdist_wheel - - name: Publish đŸ“Ļ to PyPI - if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@master - with: - password: ${{ secrets.pypi_prod_token }} \ No newline at end of file + - uses: actions/checkout@master + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + - name: Build a binary wheel and a source tarball + run: >- + python setup.py sdist bdist_wheel + - name: Publish đŸ“Ļ to PyPI + if: startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@master + with: + password: ${{ secrets.pypi-prod-token }} \ No newline at end of file diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index 1e000c25e..b4b7151da 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -3,13 +3,14 @@ name: Publish đŸ“Ļ to TestPyPI on: create: tags: - - '*' + - '*' jobs: build-n-publish: name: Build and publish đŸ“Ļ to TestPyPI runs-on: ubuntu-latest steps: +<<<<<<< HEAD - uses: actions/checkout@master - name: Set up Python 3.7 uses: actions/setup-python@v1 @@ -36,3 +37,22 @@ jobs: # password: ${{ secrets.pypi_password }} ======= >>>>>>> Add publish to test pypi workflow +======= + - uses: actions/checkout@master + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + - name: Build a binary wheel and a source tarball + run: >- + python setup.py sdist bdist_wheel + - name: Publish đŸ“Ļ to Test PyPI + uses: pypa/gh-action-pypi-publish@master + with: + password: ${{ secrets.pypi-test-token }} + repository_url: https://test.pypi.org/legacy/ +>>>>>>> Update workflows From dbabf701a19bf40c8fb2964d4a9ef73925343d05 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 20 Oct 2020 18:22:50 +0200 Subject: [PATCH 309/333] Move version file --- version.py => src/pyatmo/version.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename version.py => src/pyatmo/version.py (100%) diff --git a/version.py b/src/pyatmo/version.py similarity index 100% rename from version.py rename to src/pyatmo/version.py From 611e43f8dc0a3094065aa9ecd108a7f7ef84fd9b Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 21 Oct 2020 11:31:20 +0200 Subject: [PATCH 310/333] Update publish-to-test-pypi.yml --- .github/workflows/publish-to-test-pypi.yml | 29 ---------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index b4b7151da..230e052da 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -10,34 +10,6 @@ jobs: name: Build and publish đŸ“Ļ to TestPyPI runs-on: ubuntu-latest steps: -<<<<<<< HEAD - - uses: actions/checkout@master - - name: Set up Python 3.7 - uses: actions/setup-python@v1 - with: - python-version: 3.7 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel - - name: Build a binary wheel and a source tarball - run: >- - python setup.py sdist bdist_wheel - - name: Publish đŸ“Ļ to Test PyPI - uses: pypa/gh-action-pypi-publish@master - with: - username: "__token__" - password: ${{ secrets.pypi_test_token }} - repository_url: https://test.pypi.org/legacy/ -<<<<<<< HEAD - # - name: Publish distribution đŸ“Ļ to PyPI - # if: startsWith(github.ref, 'refs/tags') - # uses: pypa/gh-action-pypi-publish@master - # with: - # password: ${{ secrets.pypi_password }} -======= ->>>>>>> Add publish to test pypi workflow -======= - uses: actions/checkout@master - name: Set up Python 3.7 uses: actions/setup-python@v1 @@ -55,4 +27,3 @@ jobs: with: password: ${{ secrets.pypi-test-token }} repository_url: https://test.pypi.org/legacy/ ->>>>>>> Update workflows From a48b4f21a68d7c83badd21e00d26c992cc7d947b Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 21 Oct 2020 11:31:42 +0200 Subject: [PATCH 311/333] Delete version.py --- src/pyatmo/version.py | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 src/pyatmo/version.py diff --git a/src/pyatmo/version.py b/src/pyatmo/version.py deleted file mode 100644 index 3b24e4a33..000000000 --- a/src/pyatmo/version.py +++ /dev/null @@ -1,4 +0,0 @@ -MAJOR_VERSION = 4 -MINOR_VERSION = 1 -PATCH_VERSION = "0.dev0" -__version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}" From cd963b2ff3a4ef4f484997d933e10660436c0708 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 12:47:30 +0100 Subject: [PATCH 312/333] Update actions/setup-python requirement to v2.1.4 (#141) Updates the requirements on [actions/setup-python](https://github.com/actions/setup-python) to permit the latest version. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/commits/41b7212b1668f5de9d65e9c82aa777e6bbedb3a8) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish-to-pypi.yml | 2 +- .github/workflows/publish-to-test-pypi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 34773fa9d..09ff8c5ac 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@master - name: Set up Python 3.7 - uses: actions/setup-python@v1 + uses: actions/setup-python@v2.1.4 with: python-version: 3.7 - name: Install dependencies diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index 230e052da..f0a19d681 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@master - name: Set up Python 3.7 - uses: actions/setup-python@v1 + uses: actions/setup-python@v2.1.4 with: python-version: 3.7 - name: Install dependencies From 5dd27f3e6c9a56a0c2da320c79093a59854b7025 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2020 01:12:17 +0100 Subject: [PATCH 313/333] Bump cryptography from 3.1.1 to 3.2 (#142) Bumps [cryptography](https://github.com/pyca/cryptography) from 3.1.1 to 3.2. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/3.1.1...3.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 195 +++++++++++++++++++++++++-------------------------- 1 file changed, 95 insertions(+), 100 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index bfe27cd42..4cbadc92c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -35,7 +35,6 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "oauthlib": { @@ -43,7 +42,6 @@ "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.1.0" }, "pyatmo": { @@ -61,8 +59,7 @@ "requests-oauthlib": { "hashes": [ "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", - "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", - "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" + "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" ], "index": "pypi", "version": "==1.3.0" @@ -72,7 +69,6 @@ "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.25.11" } }, @@ -89,7 +85,6 @@ "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], - "markers": "python_version >= '3.5'", "version": "==2.4.2" }, "attrs": { @@ -97,7 +92,6 @@ "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.2.0" }, "black": { @@ -168,7 +162,6 @@ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], - "markers": "python_full_version >= '3.6.1'", "version": "==3.2.0" }, "chardet": { @@ -183,7 +176,6 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "colorama": { @@ -191,7 +183,6 @@ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.4.4" }, "coverage": { @@ -231,36 +222,35 @@ "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==5.3" }, "cryptography": { "hashes": [ - "sha256:21b47c59fcb1c36f1113f3709d37935368e34815ea1d7073862e92f810dc7499", - "sha256:451cdf60be4dafb6a3b78802006a020e6cd709c22d240f94f7a0696240a17154", - "sha256:4549b137d8cbe3c2eadfa56c0c858b78acbeff956bd461e40000b2164d9167c6", - "sha256:48ee615a779ffa749d7d50c291761dc921d93d7cf203dca2db663b4f193f0e49", - "sha256:559d622aef2a2dff98a892eef321433ba5bc55b2485220a8ca289c1ecc2bd54f", - "sha256:5d52c72449bb02dd45a773a203196e6d4fae34e158769c896012401f33064396", - "sha256:65beb15e7f9c16e15934569d29fb4def74ea1469d8781f6b3507ab896d6d8719", - "sha256:680da076cad81cdf5ffcac50c477b6790be81768d30f9da9e01960c4b18a66db", - "sha256:762bc5a0df03c51ee3f09c621e1cee64e3a079a2b5020de82f1613873d79ee70", - "sha256:89aceb31cd5f9fc2449fe8cf3810797ca52b65f1489002d58fe190bfb265c536", - "sha256:983c0c3de4cb9fcba68fd3f45ed846eb86a2a8b8d8bc5bb18364c4d00b3c61fe", - "sha256:99d4984aabd4c7182050bca76176ce2dbc9fa9748afe583a7865c12954d714ba", - "sha256:9d9fc6a16357965d282dd4ab6531013935425d0dc4950df2e0cf2a1b1ac1017d", - "sha256:a7597ffc67987b37b12e09c029bd1dc43965f75d328076ae85721b84046e9ca7", - "sha256:ab010e461bb6b444eaf7f8c813bb716be2d78ab786103f9608ffd37a4bd7d490", - "sha256:b12e715c10a13ca1bd27fbceed9adc8c5ff640f8e1f7ea76416352de703523c8", - "sha256:b2bded09c578d19e08bd2c5bb8fed7f103e089752c9cf7ca7ca7de522326e921", - "sha256:b372026ebf32fe2523159f27d9f0e9f485092e43b00a5adacf732192a70ba118", - "sha256:cb179acdd4ae1e4a5a160d80b87841b3d0e0be84af46c7bb2cd7ece57a39c4ba", - "sha256:e97a3b627e3cb63c415a16245d6cef2139cca18bb1183d1b9375a1c14e83f3b3", - "sha256:f0e099fc4cc697450c3dd4031791559692dd941a95254cb9aeded66a7aa8b9bc", - "sha256:f99317a0fa2e49917689b8cf977510addcfaaab769b3f899b9c481bbd76730c2" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.1.1" + "sha256:22f8251f68953553af4f9c11ec5f191198bc96cff9f0ac5dd5ff94daede0ee6d", + "sha256:284e275e3c099a80831f9898fb5c9559120d27675c3521278faba54e584a7832", + "sha256:3e17d02941c0f169c5b877597ca8be895fca0e5e3eb882526a74aa4804380a98", + "sha256:52a47e60953679eea0b4d490ca3c241fb1b166a7b161847ef4667dfd49e7699d", + "sha256:57b8c1ed13b8aa386cabbfde3be175d7b155682470b0e259fecfe53850967f8a", + "sha256:6a8f64ed096d13f92d1f601a92d9fd1f1025dc73a2ca1ced46dcf5e0d4930943", + "sha256:6e8a3c7c45101a7eeee93102500e1b08f2307c717ff553fcb3c1127efc9b6917", + "sha256:7ef41304bf978f33cfb6f43ca13bb0faac0c99cda33693aa20ad4f5e34e8cb8f", + "sha256:87c2fffd61e934bc0e2c927c3764c20b22d7f5f7f812ee1a477de4c89b044ca6", + "sha256:88069392cd9a1e68d2cfd5c3a2b0d72a44ef3b24b8977a4f7956e9e3c4c9477a", + "sha256:8a0866891326d3badb17c5fd3e02c926b635e8923fa271b4813cd4d972a57ff3", + "sha256:8f0fd8b0751d75c4483c534b209e39e918f0d14232c0d8a2a76e687f64ced831", + "sha256:9a07e6d255053674506091d63ab4270a119e9fc83462c7ab1dbcb495b76307af", + "sha256:9a8580c9afcdcddabbd064c0a74f337af74ff4529cdf3a12fa2e9782d677a2e5", + "sha256:bd80bc156d3729b38cb227a5a76532aef693b7ac9e395eea8063ee50ceed46a5", + "sha256:d1cbc3426e6150583b22b517ef3720036d7e3152d428c864ff0f3fcad2b97591", + "sha256:e15ac84dcdb89f92424cbaca4b0b34e211e7ce3ee7b0ec0e4f3c55cee65fae5a", + "sha256:e4789b84f8dedf190148441f7c5bfe7244782d9cbb194a36e17b91e7d3e1cca9", + "sha256:f01c9116bfb3ad2831e125a73dcd957d173d6ddca7701528eff1e7d97972872c", + "sha256:f0e3986f6cce007216b23c490f093f35ce2068f3c244051e559f647f6731b7ae", + "sha256:f2aa3f8ba9e2e3fd49bd3de743b976ab192fbf0eb0348cebde5d2a9de0090a9f", + "sha256:fb70a4cedd69dc52396ee114416a3656e011fb0311fca55eb55c7be6ed9c8aef" + ], + "index": "pypi", + "version": "==3.2" }, "distlib": { "hashes": [ @@ -305,7 +295,6 @@ "sha256:3139bf72d81dfd785b0a464e2776bd59bdc725b4cc10e6cf46b56a0db931c82e", "sha256:969d844b7a85d32a5f9ac4e163df6e846d73c87c8b75847494ee8f4bd2186421" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.5.6" }, "idna": { @@ -313,9 +302,16 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, + "importlib-metadata": { + "hashes": [ + "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da", + "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3" + ], + "markers": "python_version < '3.8'", + "version": "==2.0.0" + }, "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", @@ -344,7 +340,6 @@ "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466" ], - "markers": "python_version >= '3.6'", "version": "==21.4.0" }, "lazy-object-proxy": { @@ -371,7 +366,6 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -420,7 +414,6 @@ "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.4" }, "pathspec": { @@ -432,17 +425,16 @@ }, "pkginfo": { "hashes": [ - "sha256:78d032b5888ec06d7f9d18fbf8c0549a6a3477081b34cb769119a07183624fc1", - "sha256:dd008e95b13141ddd05d7e8881f0c0366a998ab90b25c2db794a1714b71583cc" + "sha256:a6a4ac943b496745cec21f14f021bbd869d5e9b4f6ec06918cffea5a2f4b9193", + "sha256:ce14d7296c673dc4c61c759a0b6c14bae34e34eb819c0017bb6ca5b7292c56e9" ], - "version": "==1.6.0" + "version": "==1.6.1" }, "pluggy": { "hashes": [ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.13.1" }, "pre-commit": { @@ -458,7 +450,6 @@ "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.9.0" }, "pycodestyle": { @@ -466,7 +457,6 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pycparser": { @@ -474,7 +464,6 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pyflakes": { @@ -482,16 +471,14 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pygments": { "hashes": [ - "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998", - "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7" + "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0", + "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773" ], - "markers": "python_version >= '3.5'", - "version": "==2.7.1" + "version": "==2.7.2" }, "pylint": { "hashes": [ @@ -503,11 +490,10 @@ }, "pyparsing": { "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + "sha256:1060635ca5ac864c2b7bc7b05a448df4e32d7d8c65e33cbe1514810d339672a2", + "sha256:56a551039101858c9e189ac9e66e330a03fb7079e97ba6b50193643905f450ce" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", - "version": "==2.4.7" + "version": "==3.0.0a2" }, "pytest": { "hashes": [ @@ -538,7 +524,6 @@ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.1" }, "pyyaml": { @@ -559,42 +544,42 @@ }, "readme-renderer": { "hashes": [ - "sha256:3176d93d2c21960fb7f7458073b9f1e5dd14a5af0ec6af4afb957337dbb6a326", - "sha256:e6871b10341cdd85ade112fb8503b31301d1ca12c0c3b3e1358855329519968b" + "sha256:267854ac3b1530633c2394ead828afcd060fc273217c42ac36b6be9c42cd9a9d", + "sha256:6b7e5aa59210a40de72eb79931491eaf46fefca2952b9181268bd7c7c65c260a" ], - "version": "==27.0" + "version": "==28.0" }, "regex": { "hashes": [ - "sha256:02686a2f0b1a4be0facdd0d3ad4dc6c23acaa0f38fb5470d892ae88584ba705c", - "sha256:137da580d1e6302484be3ef41d72cf5c3ad22a076070051b7449c0e13ab2c482", - "sha256:20cdd7e1736f4f61a5161aa30d05ac108ab8efc3133df5eb70fe1e6a23ea1ca6", - "sha256:25991861c6fef1e5fd0a01283cf5658c5e7f7aa644128e85243bc75304e91530", - "sha256:26b85672275d8c7a9d4ff93dbc4954f5146efdb2ecec89ad1de49439984dea14", - "sha256:2f60ba5c33f00ce9be29a140e6f812e39880df8ba9cb92ad333f0016dbc30306", - "sha256:3dd952f3f8dc01b72c0cf05b3631e05c50ac65ddd2afdf26551638e97502107b", - "sha256:578ac6379e65eb8e6a85299b306c966c852712c834dc7eef0ba78d07a828f67b", - "sha256:5d4a3221f37520bb337b64a0632716e61b26c8ae6aaffceeeb7ad69c009c404b", - "sha256:608d6c05452c0e6cc49d4d7407b4767963f19c4d2230fa70b7201732eedc84f2", - "sha256:65b6b018b07e9b3b6a05c2c3bb7710ed66132b4df41926c243887c4f1ff303d5", - "sha256:698f8a5a2815e1663d9895830a063098ae2f8f2655ae4fdc5dfa2b1f52b90087", - "sha256:6c72adb85adecd4522a488a751e465842cdd2a5606b65464b9168bf029a54272", - "sha256:6d4cdb6c20e752426b2e569128488c5046fb1b16b1beadaceea9815c36da0847", - "sha256:6e9f72e0ee49f7d7be395bfa29e9533f0507a882e1e6bf302c0a204c65b742bf", - "sha256:828618f3c3439c5e6ef8621e7c885ca561bbaaba90ddbb6a7dfd9e1ec8341103", - "sha256:85b733a1ef2b2e7001aff0e204a842f50ad699c061856a214e48cfb16ace7d0c", - "sha256:8958befc139ac4e3f16d44ec386c490ea2121ed8322f4956f83dd9cad8e9b922", - "sha256:a51e51eecdac39a50ede4aeed86dbef4776e3b73347d31d6ad0bc9648ba36049", - "sha256:aeac7c9397480450016bc4a840eefbfa8ca68afc1e90648aa6efbfe699e5d3bb", - "sha256:aef23aed9d4017cc74d37f703d57ce254efb4c8a6a01905f40f539220348abf9", - "sha256:af1f5e997dd1ee71fb6eb4a0fb6921bf7a778f4b62f1f7ef0d7445ecce9155d6", - "sha256:b5eeaf4b5ef38fab225429478caf71f44d4a0b44d39a1aa4d4422cda23a9821b", - "sha256:d25f5cca0f3af6d425c9496953445bf5b288bb5b71afc2b8308ad194b714c159", - "sha256:d81be22d5d462b96a2aa5c512f741255ba182995efb0114e5a946fe254148df1", - "sha256:e935a166a5f4c02afe3f7e4ce92ce5a786f75c6caa0c4ce09c922541d74b77e8", - "sha256:ef3a55b16c6450574734db92e0a3aca283290889934a23f7498eaf417e3af9f0" - ], - "version": "==2020.10.15" + "sha256:0cb23ed0e327c18fb7eac61ebbb3180ebafed5b9b86ca2e15438201e5903b5dd", + "sha256:1a065e7a6a1b4aa851a0efa1a2579eabc765246b8b3a5fd74000aaa3134b8b4e", + "sha256:1a511470db3aa97432ac8c1bf014fcc6c9fbfd0f4b1313024d342549cf86bcd6", + "sha256:1c447b0d108cddc69036b1b3910fac159f2b51fdeec7f13872e059b7bc932be1", + "sha256:2278453c6a76280b38855a263198961938108ea2333ee145c5168c36b8e2b376", + "sha256:240509721a663836b611fa13ca1843079fc52d0b91ef3f92d9bba8da12e768a0", + "sha256:4e21340c07090ddc8c16deebfd82eb9c9e1ec5e62f57bb86194a2595fd7b46e0", + "sha256:570e916a44a361d4e85f355aacd90e9113319c78ce3c2d098d2ddf9631b34505", + "sha256:59d5c6302d22c16d59611a9fd53556554010db1d47e9df5df37be05007bebe75", + "sha256:6a46eba253cedcbe8a6469f881f014f0a98819d99d341461630885139850e281", + "sha256:6f567df0601e9c7434958143aebea47a9c4b45434ea0ae0286a4ec19e9877169", + "sha256:781906e45ef1d10a0ed9ec8ab83a09b5e0d742de70e627b20d61ccb1b1d3964d", + "sha256:8469377a437dbc31e480993399fd1fd15fe26f382dc04c51c9cb73e42965cc06", + "sha256:8cd0d587aaac74194ad3e68029124c06245acaeddaae14cb45844e5c9bebeea4", + "sha256:97a023f97cddf00831ba04886d1596ef10f59b93df7f855856f037190936e868", + "sha256:a973d5a7a324e2a5230ad7c43f5e1383cac51ef4903bf274936a5634b724b531", + "sha256:af360e62a9790e0a96bc9ac845d87bfa0e4ee0ee68547ae8b5a9c1030517dbef", + "sha256:b706c70070eea03411b1761fff3a2675da28d042a1ab7d0863b3efe1faa125c9", + "sha256:bfd7a9fddd11d116a58b62ee6c502fd24cfe22a4792261f258f886aa41c2a899", + "sha256:c30d8766a055c22e39dd7e1a4f98f6266169f2de05db737efe509c2fb9c8a3c8", + "sha256:c53dc8ee3bb7b7e28ee9feb996a0c999137be6c1d3b02cb6b3c4cba4f9e5ed09", + "sha256:c95d514093b80e5309bdca5dd99e51bcf82c44043b57c34594d9d7556bd04d05", + "sha256:d43cf21df524283daa80ecad551c306b7f52881c8d0fe4e3e76a96b626b6d8d8", + "sha256:d62205f00f461fe8b24ade07499454a3b7adf3def1225e258b994e2215fd15c5", + "sha256:e289a857dca3b35d3615c3a6a438622e20d1bf0abcb82c57d866c8d0be3f44c4", + "sha256:e5f6aa56dda92472e9d6f7b1e6331f4e2d51a67caafff4d4c5121cadac03941e", + "sha256:f4b1c65ee86bfbf7d0c3dfd90592a9e3d6e9ecd36c367c884094c050d4c35d04" + ], + "version": "==2020.10.23" }, "requests": { "hashes": [ @@ -639,7 +624,6 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.15.0" }, "toml": { @@ -659,11 +643,10 @@ }, "tqdm": { "hashes": [ - "sha256:43ca183da3367578ebf2f1c2e3111d51ea161ed1dc4e6345b86e27c2a93beff7", - "sha256:69dfa6714dee976e2425a9aab84b622675b7b1742873041e3db8a8e86132a4af" + "sha256:9ad44aaf0fc3697c06f6e05c7cf025dd66bc7bcb7613c66d85f4464c47ac8fad", + "sha256:ef54779f1c09f346b2b5a8e5c61f96fbcb639929e640e59f8cf810794f406432" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1'", - "version": "==4.50.2" + "version": "==4.51.0" }, "twine": { "hashes": [ @@ -677,9 +660,11 @@ "hashes": [ "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d", "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c", "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", @@ -688,15 +673,20 @@ "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395", "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", + "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], + "markers": "implementation_name == 'cpython' and python_version < '3.8'", "version": "==1.4.1" }, "typing-extensions": { @@ -712,16 +702,14 @@ "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.25.11" }, "virtualenv": { "hashes": [ - "sha256:0ebc633426d7468664067309842c81edab11ae97fcaf27e8ad7f5748c89b431b", - "sha256:2a72c80fa2ad8f4e2985c06e6fc12c3d60d060e410572f553c90619b0f6efaf3" + "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", + "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.0.35" + "version": "==20.1.0" }, "webencodings": { "hashes": [ @@ -735,6 +723,13 @@ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" ], "version": "==1.12.1" + }, + "zipp": { + "hashes": [ + "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108", + "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb" + ], + "version": "==3.4.0" } } } From d1182bfff77c0cde5bec44c88872e82a49f6bf2b Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Wed, 28 Oct 2020 11:32:27 +0100 Subject: [PATCH 314/333] Fix secrets variable name Signed-off-by: Hugo Dupras --- .github/workflows/publish-to-pypi.yml | 2 +- .github/workflows/publish-to-test-pypi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 09ff8c5ac..399cac0cd 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -28,4 +28,4 @@ jobs: if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@master with: - password: ${{ secrets.pypi-prod-token }} \ No newline at end of file + password: ${{ secrets.pypi_prod_token }} diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index f0a19d681..b3fec8647 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -25,5 +25,5 @@ jobs: - name: Publish đŸ“Ļ to Test PyPI uses: pypa/gh-action-pypi-publish@master with: - password: ${{ secrets.pypi-test-token }} + password: ${{ secrets.pypi_test_token }} repository_url: https://test.pypi.org/legacy/ From 332379e5c8bccc7acc0b7f9c7f7353b5b37d3b8b Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Wed, 21 Oct 2020 14:22:04 +0200 Subject: [PATCH 315/333] Tune-up some method Signed-off-by: Hugo Dupras --- src/pyatmo/camera.py | 121 ++++++++++++++++----------------------- src/pyatmo/thermostat.py | 56 +++++++----------- 2 files changed, 70 insertions(+), 107 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index d0d0196cb..5e238d3fc 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -60,40 +60,44 @@ def __init__(self, auth: NetatmoOAuth2, size: int = 30) -> None: for item in self.raw_data: home_id: str = item.get("id", "") - home_name: str = item.get("name", "") - if not home_name: - home_name = "Unknown" - self.homes[home_id]["name"] = home_name + if not item.get("name"): + self.homes[home_id]["name"] = "Unknown" - for person in item["persons"]: + for person in item.get("persons", []): self.persons[person["id"]] = person for event in item.get("events", []): - if event["type"] == "outdoor": - self.outdoor_events[event["camera_id"]][event["time"]] = event + self._store_events(event) - else: - self.events[event["camera_id"]][event["time"]] = event - - for camera in item["cameras"]: + for camera in item.get("cameras", []): self.cameras[home_id][camera["id"]] = camera self.cameras[home_id][camera["id"]]["home_id"] = home_id + self.types[home_id][camera["type"]] = camera if camera["type"] == "NACamera": for module in camera.get("modules", []): self.modules[module["id"]] = module self.modules[module["id"]]["cam_id"] = camera["id"] - for smoke in item["smokedetectors"]: + for smoke in item.get("smokedetectors", []): self.smokedetectors[home_id][smoke["id"]] = smoke + self.types[home_id][smoke["type"]] = smoke + + self._store_last_event() + + for home_id in self.homes: + for camera_id in self.cameras[home_id]: + self.update_camera_urls(camera_id) - for camera_type in item["cameras"]: - self.types[home_id][camera_type["type"]] = camera_type + def _store_events(self, event): + if event["type"] == "outdoor": + self.outdoor_events[event["camera_id"]][event["time"]] = event - for smoke_type in item["smokedetectors"]: - self.types[home_id][smoke_type["type"]] = smoke_type + else: + self.events[event["camera_id"]][event["time"]] = event + def _store_last_event(self): for camera in self.events: self.last_event[camera] = self.events[camera][ sorted(self.events[camera])[-1] @@ -104,13 +108,9 @@ def __init__(self, auth: NetatmoOAuth2, size: int = 30) -> None: sorted(self.outdoor_events[camera])[-1] ] - for home_id in self.homes: - for camera_id in self.cameras[home_id]: - self.update_camera_urls(camera_id) - def get_camera(self, camera_id: str) -> Dict[str, str]: """Get camera data.""" - for home_id, _ in self.cameras.items(): + for home_id in self.cameras: if camera_id in self.cameras[home_id]: return self.cameras[home_id][camera_id] @@ -122,7 +122,7 @@ def get_module(self, module_id: str) -> Optional[dict]: def get_smokedetector(self, smoke_id: str) -> Optional[dict]: """Get smoke detector.""" - for home_id, _ in self.smokedetectors.items(): + for home_id in self.smokedetectors: if smoke_id in self.smokedetectors[home_id]: return self.smokedetectors[home_id][smoke_id] @@ -220,7 +220,7 @@ def get_person_id(self, name: str) -> Optional[str]: str -- ID of a person """ for pid, data in self.persons.items(): - if "pseudo" in data and name == data["pseudo"]: + if name == data.get("pseudo"): return pid return None @@ -242,10 +242,7 @@ def get_camera_picture( def get_profile_image(self, name: str) -> Tuple[Optional[bytes], Optional[str]]: """Retrieve the face of a given person.""" for person in self.persons: - if ( - "pseudo" in self.persons[person] - and name == self.persons[person]["pseudo"] - ): + if name == self.persons[person].get("pseudo"): image_id = self.persons[person]["face"]["id"] key = self.persons[person]["face"]["key"] return self.get_camera_picture(image_id, key) @@ -267,25 +264,15 @@ def get_event_id(data: Dict): events = {e["time"]: e for e in data.values()} return min(events.items())[1].get("id") - if device_type == "NACamera": - # for the Welcome camera - if not event_id: - # If no event is provided we need to retrieve the oldest of - # the last event seen by each camera + if not event_id: + # If no event is provided we need to retrieve the oldest of + # the last event seen by each camera + if device_type == "NACamera": + # for the Welcome camera event_id = get_event_id(self.last_event) - elif device_type == "NOC": - # for the Presence camera - if not event_id: - # If no event is provided we need to retrieve the oldest of - # the last event seen by each camera - event_id = get_event_id(self.outdoor_last_event) - - elif device_type == "NSD": - # for the smoke detector - if not event_id: - # If no event is provided we need to retrieve the oldest of - # the last event by each smoke detector + elif device_type in {"NOC", "NSD"}: + # for the Presence camera and for the smoke detector event_id = get_event_id(self.outdoor_last_event) post_params = { @@ -309,21 +296,9 @@ def get_event_id(data: Dict): LOG.debug("No resp received") for event in event_list: - if event["type"] == "outdoor": - self.outdoor_events[event["camera_id"]][event["time"]] = event - - else: - self.events[event["camera_id"]][event["time"]] = event + self._store_events(event) - for camera in self.events: - self.last_event[camera] = self.events[camera][ - sorted(self.events[camera])[-1] - ] - - for camera in self.outdoor_events: - self.outdoor_last_event[camera] = self.outdoor_events[camera][ - sorted(self.outdoor_events[camera])[-1] - ] + self._store_last_event() def person_seen_by_camera( self, @@ -333,6 +308,14 @@ def person_seen_by_camera( ) -> bool: """Evaluate if a specific person has been seen.""" # Check in the last event is someone known has been seen + def _person_in_event(current_event, name): + if current_event["type"] == "person": + person_id = current_event["person_id"] + + if self.persons[person_id].get("pseudo") == name: + return True + return None + if exclude: limit = time.time() - exclude array_time_event = sorted(self.events[camera_id], reverse=True) @@ -341,23 +324,15 @@ def person_seen_by_camera( if time_ev < limit: return False - if self.events[camera_id][time_ev]["type"] == "person": - person_id = self.events[camera_id][time_ev]["person_id"] - - if ( - "pseudo" in self.persons[person_id] - and self.persons[person_id]["pseudo"] == name - ): - return True + current_event = self.events[camera_id][time_ev] + if _person_in_event(current_event, name) is True: + return True - elif self.last_event[camera_id]["type"] == "person": - person_id = self.last_event[camera_id]["person_id"] + return False - if ( - "pseudo" in self.persons[person_id] - and self.persons[person_id]["pseudo"] == name - ): - return True + current_event = self.last_event[camera_id] + if _person_in_event(current_event, name) is True: + return True return False diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index 7187fbc8d..0e5180c00 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -55,28 +55,28 @@ def __init__(self, auth: NetatmoOAuth2) -> None: home_name = "Unknown" self.homes[home_id]["name"] = home_name - if "modules" in item: + if "modules" not in item: + continue - for module in item["modules"]: - self.modules[home_id][module["id"]] = module + for module in item["modules"]: + self.modules[home_id][module["id"]] = module - if "therm_setpoint_default_duration" in item: - self.setpoint_duration[home_id] = item[ - "therm_setpoint_default_duration" - ] + self.setpoint_duration[home_id] = item.get( + "therm_setpoint_default_duration", + ) - for room in item.get("rooms", []): - self.rooms[home_id][room["id"]] = room + for room in item.get("rooms", []): + self.rooms[home_id][room["id"]] = room - for schedule in item.get("therm_schedules", []): - schedule_id = schedule["id"] - self.schedules[home_id][schedule_id] = schedule + for schedule in item.get("therm_schedules", []): + schedule_id = schedule["id"] + self.schedules[home_id][schedule_id] = schedule - if schedule_id not in self.zones[home_id]: - self.zones[home_id][schedule_id] = {} + if schedule_id not in self.zones[home_id]: + self.zones[home_id][schedule_id] = {} - for zone in schedule["zones"]: - self.zones[home_id][schedule_id][zone["id"]] = zone + for zone in schedule["zones"]: + self.zones[home_id][schedule_id][zone["id"]] = zone def _get_selected_schedule(self, home_id: str) -> Dict: """Get the selected schedule for a given home ID.""" @@ -138,34 +138,22 @@ def __init__(self, auth: NetatmoOAuth2, home_id: str): self.raw_data = resp["body"]["home"] self.rooms: Dict = {} - self.thermostats: Dict = {} - self.valves: Dict = {} - self.relays: Dict = {} + self.thermostats: Dict = defaultdict(dict) + self.valves: Dict = defaultdict(dict) + self.relays: Dict = defaultdict(dict) for room in self.raw_data.get("rooms", []): self.rooms[room["id"]] = room for module in self.raw_data.get("modules", []): if module["type"] == "NATherm1": - thermostat_id = module["id"] - if thermostat_id not in self.thermostats: - self.thermostats[thermostat_id] = {} - - self.thermostats[thermostat_id] = module + self.thermostats[module["id"]] = module elif module["type"] == "NRV": - valve_id = module["id"] - if valve_id not in self.valves: - self.valves[valve_id] = {} - - self.valves[valve_id] = module + self.valves[module["id"]] = module elif module["type"] == "NAPlug": - relay_id = module["id"] - if relay_id not in self.relays: - self.relays[relay_id] = {} - - self.relays[relay_id] = module + self.relays[module["id"]] = module def get_room(self, room_id: str) -> Dict: for key, value in self.rooms.items(): From 4cd1486fcff4f045e05735e480c07021a1ef01df Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Thu, 22 Oct 2020 11:54:40 +0200 Subject: [PATCH 316/333] Extract some complex epression Signed-off-by: Hugo Dupras --- src/pyatmo/camera.py | 134 +++++++++++++++++----------------- src/pyatmo/public_data.py | 16 ++-- src/pyatmo/weather_station.py | 3 + 3 files changed, 78 insertions(+), 75 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 5e238d3fc..2fc4df67c 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -72,9 +72,9 @@ def __init__(self, auth: NetatmoOAuth2, size: int = 30) -> None: for camera in item.get("cameras", []): self.cameras[home_id][camera["id"]] = camera - self.cameras[home_id][camera["id"]]["home_id"] = home_id - self.types[home_id][camera["type"]] = camera + + self.cameras[home_id][camera["id"]]["home_id"] = home_id if camera["type"] == "NACamera": for module in camera.get("modules", []): self.modules[module["id"]] = module @@ -308,11 +308,11 @@ def person_seen_by_camera( ) -> bool: """Evaluate if a specific person has been seen.""" # Check in the last event is someone known has been seen - def _person_in_event(current_event, name): - if current_event["type"] == "person": - person_id = current_event["person_id"] + def _person_in_event(curr_event, person_name): + if curr_event["type"] == "person": + person_id = curr_event["person_id"] - if self.persons[person_id].get("pseudo") == name: + if self.persons[person_id].get("pseudo") == person_name: return True return None @@ -361,18 +361,17 @@ def someone_known_seen(self, camera_id: str, exclude: int = 0) -> bool: if time_ev < limit: return False - if self.events[camera_id][time_ev]["type"] == "person": - if ( - self.events[camera_id][time_ev]["person_id"] - in self._known_persons() - ): + curr_event = self.events[camera_id][time_ev] + if curr_event["type"] == "person": + if curr_event["person_id"] in self._known_persons(): return True # Check in the last event if someone known has been seen - elif self.last_event[camera_id]["type"] == "person": - - if self.last_event[camera_id]["person_id"] in self._known_persons(): - return True + else: + curr_event = self.last_event[camera_id] + if curr_event["type"] == "person": + if curr_event["person_id"] in self._known_persons(): + return True return False @@ -389,19 +388,17 @@ def someone_unknown_seen(self, camera_id: str, exclude: int = 0) -> bool: if time_ev < limit: return False - if self.events[camera_id][time_ev]["type"] == "person": - - if ( - self.events[camera_id][time_ev]["person_id"] - not in self._known_persons() - ): + curr_event = self.events[camera_id][time_ev] + if curr_event["type"] == "person": + if curr_event["person_id"] not in self._known_persons(): return True # Check in the last event is noone known has been seen - elif self.last_event[camera_id]["type"] == "person": - - if self.last_event[camera_id]["person_id"] not in self._known_persons(): - return True + else: + curr_event = self.last_event[camera_id] + if curr_event["type"] == "person": + if curr_event["person_id"] not in self._known_persons(): + return True return False @@ -428,45 +425,38 @@ def motion_detected(self, camera_id: str, exclude: int = 0) -> bool: def outdoor_motion_detected(self, camera_id: str, offset: int = 0) -> bool: """Evaluate if outdoor movement has been detected.""" + if camera_id not in self.last_event: + return False + + last_event = self.last_event[camera_id] return ( - camera_id in self.last_event - and self.last_event[camera_id]["type"] == "movement" - and self.last_event[camera_id]["video_status"] == "recording" - and self.last_event[camera_id]["time"] + offset > int(time.time()) + last_event["type"] == "movement" + and last_event["video_status"] == "recording" + and last_event["time"] + offset > int(time.time()) ) - def human_detected(self, camera_id: str, offset: int = 0) -> bool: + def _stuff_detected(self, stuff_name: str, camera_id: str, offset: int) -> bool: """Evaluate if a human has been detected.""" if self.outdoor_last_event[camera_id]["video_status"] == "recording": for event in self.outdoor_last_event[camera_id]["event_list"]: - if event["type"] == "human" and event["time"] + offset > int( - time.time(), + if event["type"] == stuff_name and ( + event["time"] + offset > int(time.time()) ): return True return False + def human_detected(self, camera_id: str, offset: int = 0) -> bool: + """Evaluate if a human has been detected.""" + return self._stuff_detected("human", camera_id, offset) + def animal_detected(self, camera_id: str, offset: int = 0) -> bool: """Evaluate if an animal has been detected.""" - if self.outdoor_last_event[camera_id]["video_status"] == "recording": - for event in self.outdoor_last_event[camera_id]["event_list"]: - if event["type"] == "animal" and event["time"] + offset > int( - time.time(), - ): - return True - - return False + return self._stuff_detected("animal", camera_id, offset) def car_detected(self, camera_id: str, offset: int = 0) -> bool: """Evaluate if a car has been detected.""" - if self.outdoor_last_event[camera_id]["video_status"] == "recording": - for event in self.outdoor_last_event[camera_id]["event_list"]: - if event["type"] == "vehicle" and event["time"] + offset > int( - time.time(), - ): - return True - - return False + return self._stuff_detected("vehicle", camera_id, offset) def module_motion_detected( self, @@ -483,19 +473,23 @@ def module_motion_detected( if time_ev < limit: return False + curr_event = self.events[camera_id][time_ev] if ( - self.events[camera_id][time_ev]["type"] - in ["tag_big_move", "tag_small_move"] - and self.events[camera_id][time_ev]["module_id"] == module_id + curr_event["type"] in {"tag_big_move", "tag_small_move"} + and curr_event["module_id"] == module_id ): return True - elif ( - camera_id in self.last_event - and self.last_event[camera_id]["type"] in ["tag_big_move", "tag_small_move"] - and self.last_event[camera_id]["module_id"] == module_id - ): - return True + else: + if camera_id not in self.last_event: + return False + + curr_event = self.last_event[camera_id] + if ( + curr_event["type"] in {"tag_big_move", "tag_small_move"} + and curr_event["module_id"] == module_id + ): + return True return False @@ -509,17 +503,23 @@ def module_opened(self, module_id: str, camera_id: str, exclude: int = 0) -> boo if time_ev < limit: return False + curr_event = self.events[camera_id][time_ev] if ( - self.events[camera_id][time_ev]["type"] == "tag_open" - and self.events[camera_id][time_ev]["module_id"] == module_id + curr_event["type"] == "tag_open" + and curr_event["module_id"] == module_id ): return True - elif camera_id in self.last_event and ( - self.last_event[camera_id]["type"] == "tag_open" - and self.last_event[camera_id]["module_id"] == module_id - ): - return True + else: + if camera_id not in self.last_event: + return False + + curr_event = self.last_event[camera_id] + if ( + curr_event["type"] == "tag_open" + and curr_event["module_id"] == module_id + ): + return True return False @@ -548,15 +548,15 @@ def set_state( if floodlight: param, val = "floodlight", floodlight.lower() - if val not in ["on", "off", "auto"]: + if val not in {"on", "off", "auto"}: LOG.error("Invalid value for floodlight") else: module[param] = val if monitoring: param, val = "monitoring", monitoring.lower() - if val not in ["on", "off"]: - LOG.error("Invalid value fÃŧr monitoring") + if val not in {"on", "off"}: + LOG.error("Invalid value for monitoring") else: module[param] = val diff --git a/src/pyatmo/public_data.py b/src/pyatmo/public_data.py index 52969a003..9c0aa9e70 100644 --- a/src/pyatmo/public_data.py +++ b/src/pyatmo/public_data.py @@ -29,10 +29,10 @@ class PublicData: def __init__( self, auth: NetatmoOAuth2, - LAT_NE: str, - LON_NE: str, - LAT_SW: str, - LON_SW: str, + lat_ne: str, + lon_ne: str, + lat_sw: str, + lon_sw: str, required_data_type: str = None, filtering: bool = False, ) -> None: @@ -53,10 +53,10 @@ def __init__( """ self.auth = auth post_params: Dict = { - "lat_ne": LAT_NE, - "lon_ne": LON_NE, - "lat_sw": LAT_SW, - "lon_sw": LON_SW, + "lat_ne": lat_ne, + "lon_ne": lon_ne, + "lat_sw": lat_sw, + "lon_sw": lon_sw, "filter": filtering, } diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 9b8329ff9..59c32d2cd 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -281,6 +281,9 @@ def get_min_max_t_h( elif frame == "day": start, end = today_stamps() + else: + raise ValueError("'frame' value can only be 'last24' or 'day'") + resp = self.get_data( device_id=station_id, module_id=module_id, From b44a70bf02d12da12234eeea87af3a9ba2f1212c Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Wed, 28 Oct 2020 11:16:17 +0100 Subject: [PATCH 317/333] Apply suggestions from code review Co-authored-by: cgtobi --- src/pyatmo/camera.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index 2fc4df67c..a8561fe4f 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -435,11 +435,11 @@ def outdoor_motion_detected(self, camera_id: str, offset: int = 0) -> bool: and last_event["time"] + offset > int(time.time()) ) - def _stuff_detected(self, stuff_name: str, camera_id: str, offset: int) -> bool: + def _object_detected(self, object_name: str, camera_id: str, offset: int) -> bool: """Evaluate if a human has been detected.""" if self.outdoor_last_event[camera_id]["video_status"] == "recording": for event in self.outdoor_last_event[camera_id]["event_list"]: - if event["type"] == stuff_name and ( + if event["type"] == object_name and ( event["time"] + offset > int(time.time()) ): return True @@ -448,15 +448,15 @@ def _stuff_detected(self, stuff_name: str, camera_id: str, offset: int) -> bool: def human_detected(self, camera_id: str, offset: int = 0) -> bool: """Evaluate if a human has been detected.""" - return self._stuff_detected("human", camera_id, offset) + return self._object_detected("human", camera_id, offset) def animal_detected(self, camera_id: str, offset: int = 0) -> bool: """Evaluate if an animal has been detected.""" - return self._stuff_detected("animal", camera_id, offset) + return self._object_detected("animal", camera_id, offset) def car_detected(self, camera_id: str, offset: int = 0) -> bool: """Evaluate if a car has been detected.""" - return self._stuff_detected("vehicle", camera_id, offset) + return self._object_detected("vehicle", camera_id, offset) def module_motion_detected( self, From 3862d5c1c54472e3a33c34a28bf25f03715719da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2020 05:53:49 +0000 Subject: [PATCH 318/333] Bump pytest from 6.1.1 to 6.1.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.1.1 to 6.1.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.1.1...6.1.2) Signed-off-by: dependabot[bot] --- Pipfile.lock | 120 ++++++++++++++++++++++++++------------------------- 1 file changed, 61 insertions(+), 59 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 4cbadc92c..a2905b833 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -226,31 +226,30 @@ }, "cryptography": { "hashes": [ - "sha256:22f8251f68953553af4f9c11ec5f191198bc96cff9f0ac5dd5ff94daede0ee6d", - "sha256:284e275e3c099a80831f9898fb5c9559120d27675c3521278faba54e584a7832", - "sha256:3e17d02941c0f169c5b877597ca8be895fca0e5e3eb882526a74aa4804380a98", - "sha256:52a47e60953679eea0b4d490ca3c241fb1b166a7b161847ef4667dfd49e7699d", - "sha256:57b8c1ed13b8aa386cabbfde3be175d7b155682470b0e259fecfe53850967f8a", - "sha256:6a8f64ed096d13f92d1f601a92d9fd1f1025dc73a2ca1ced46dcf5e0d4930943", - "sha256:6e8a3c7c45101a7eeee93102500e1b08f2307c717ff553fcb3c1127efc9b6917", - "sha256:7ef41304bf978f33cfb6f43ca13bb0faac0c99cda33693aa20ad4f5e34e8cb8f", - "sha256:87c2fffd61e934bc0e2c927c3764c20b22d7f5f7f812ee1a477de4c89b044ca6", - "sha256:88069392cd9a1e68d2cfd5c3a2b0d72a44ef3b24b8977a4f7956e9e3c4c9477a", - "sha256:8a0866891326d3badb17c5fd3e02c926b635e8923fa271b4813cd4d972a57ff3", - "sha256:8f0fd8b0751d75c4483c534b209e39e918f0d14232c0d8a2a76e687f64ced831", - "sha256:9a07e6d255053674506091d63ab4270a119e9fc83462c7ab1dbcb495b76307af", - "sha256:9a8580c9afcdcddabbd064c0a74f337af74ff4529cdf3a12fa2e9782d677a2e5", - "sha256:bd80bc156d3729b38cb227a5a76532aef693b7ac9e395eea8063ee50ceed46a5", - "sha256:d1cbc3426e6150583b22b517ef3720036d7e3152d428c864ff0f3fcad2b97591", - "sha256:e15ac84dcdb89f92424cbaca4b0b34e211e7ce3ee7b0ec0e4f3c55cee65fae5a", - "sha256:e4789b84f8dedf190148441f7c5bfe7244782d9cbb194a36e17b91e7d3e1cca9", - "sha256:f01c9116bfb3ad2831e125a73dcd957d173d6ddca7701528eff1e7d97972872c", - "sha256:f0e3986f6cce007216b23c490f093f35ce2068f3c244051e559f647f6731b7ae", - "sha256:f2aa3f8ba9e2e3fd49bd3de743b976ab192fbf0eb0348cebde5d2a9de0090a9f", - "sha256:fb70a4cedd69dc52396ee114416a3656e011fb0311fca55eb55c7be6ed9c8aef" + "sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538", + "sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f", + "sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77", + "sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b", + "sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33", + "sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e", + "sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb", + "sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e", + "sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7", + "sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297", + "sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d", + "sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7", + "sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b", + "sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7", + "sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4", + "sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8", + "sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b", + "sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851", + "sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13", + "sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b", + "sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3", + "sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df" ], - "index": "pypi", - "version": "==3.2" + "version": "==3.2.1" }, "distlib": { "hashes": [ @@ -497,11 +496,11 @@ }, "pytest": { "hashes": [ - "sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9", - "sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92" + "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe", + "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e" ], "index": "pypi", - "version": "==6.1.1" + "version": "==6.1.2" }, "pytest-cov": { "hashes": [ @@ -551,35 +550,35 @@ }, "regex": { "hashes": [ - "sha256:0cb23ed0e327c18fb7eac61ebbb3180ebafed5b9b86ca2e15438201e5903b5dd", - "sha256:1a065e7a6a1b4aa851a0efa1a2579eabc765246b8b3a5fd74000aaa3134b8b4e", - "sha256:1a511470db3aa97432ac8c1bf014fcc6c9fbfd0f4b1313024d342549cf86bcd6", - "sha256:1c447b0d108cddc69036b1b3910fac159f2b51fdeec7f13872e059b7bc932be1", - "sha256:2278453c6a76280b38855a263198961938108ea2333ee145c5168c36b8e2b376", - "sha256:240509721a663836b611fa13ca1843079fc52d0b91ef3f92d9bba8da12e768a0", - "sha256:4e21340c07090ddc8c16deebfd82eb9c9e1ec5e62f57bb86194a2595fd7b46e0", - "sha256:570e916a44a361d4e85f355aacd90e9113319c78ce3c2d098d2ddf9631b34505", - "sha256:59d5c6302d22c16d59611a9fd53556554010db1d47e9df5df37be05007bebe75", - "sha256:6a46eba253cedcbe8a6469f881f014f0a98819d99d341461630885139850e281", - "sha256:6f567df0601e9c7434958143aebea47a9c4b45434ea0ae0286a4ec19e9877169", - "sha256:781906e45ef1d10a0ed9ec8ab83a09b5e0d742de70e627b20d61ccb1b1d3964d", - "sha256:8469377a437dbc31e480993399fd1fd15fe26f382dc04c51c9cb73e42965cc06", - "sha256:8cd0d587aaac74194ad3e68029124c06245acaeddaae14cb45844e5c9bebeea4", - "sha256:97a023f97cddf00831ba04886d1596ef10f59b93df7f855856f037190936e868", - "sha256:a973d5a7a324e2a5230ad7c43f5e1383cac51ef4903bf274936a5634b724b531", - "sha256:af360e62a9790e0a96bc9ac845d87bfa0e4ee0ee68547ae8b5a9c1030517dbef", - "sha256:b706c70070eea03411b1761fff3a2675da28d042a1ab7d0863b3efe1faa125c9", - "sha256:bfd7a9fddd11d116a58b62ee6c502fd24cfe22a4792261f258f886aa41c2a899", - "sha256:c30d8766a055c22e39dd7e1a4f98f6266169f2de05db737efe509c2fb9c8a3c8", - "sha256:c53dc8ee3bb7b7e28ee9feb996a0c999137be6c1d3b02cb6b3c4cba4f9e5ed09", - "sha256:c95d514093b80e5309bdca5dd99e51bcf82c44043b57c34594d9d7556bd04d05", - "sha256:d43cf21df524283daa80ecad551c306b7f52881c8d0fe4e3e76a96b626b6d8d8", - "sha256:d62205f00f461fe8b24ade07499454a3b7adf3def1225e258b994e2215fd15c5", - "sha256:e289a857dca3b35d3615c3a6a438622e20d1bf0abcb82c57d866c8d0be3f44c4", - "sha256:e5f6aa56dda92472e9d6f7b1e6331f4e2d51a67caafff4d4c5121cadac03941e", - "sha256:f4b1c65ee86bfbf7d0c3dfd90592a9e3d6e9ecd36c367c884094c050d4c35d04" - ], - "version": "==2020.10.23" + "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a", + "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f", + "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb", + "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5", + "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c", + "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53", + "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12", + "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c", + "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd", + "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504", + "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b", + "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e", + "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582", + "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0", + "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c", + "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0", + "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd", + "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e", + "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786", + "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de", + "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789", + "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520", + "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa", + "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b", + "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625", + "sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d", + "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26" + ], + "version": "==2020.10.28" }, "requests": { "hashes": [ @@ -628,10 +627,10 @@ }, "toml": { "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "version": "==0.10.1" + "version": "==0.10.2" }, "tox": { "hashes": [ @@ -671,6 +670,7 @@ "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d", "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", @@ -682,8 +682,10 @@ "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", + "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298", "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], "markers": "implementation_name == 'cpython' and python_version < '3.8'", From c1c836ff031525ed61162a877b1e92dcbe692cea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2020 05:54:49 +0000 Subject: [PATCH 319/333] Bump pre-commit from 2.7.1 to 2.8.2 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.7.1 to 2.8.2. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/master/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.7.1...v2.8.2) Signed-off-by: dependabot[bot] --- Pipfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index a2905b833..c4d8b7dc3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -438,11 +438,11 @@ }, "pre-commit": { "hashes": [ - "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a", - "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70" + "sha256:22e6aa3bd571debb01eb7d34483f11c01b65237be4eebbf30c3d4fb65762d315", + "sha256:905ebc9b534b991baec87e934431f2d0606ba27f2b90f7f652985f5a5b8b6ae6" ], "index": "pypi", - "version": "==2.7.1" + "version": "==2.8.2" }, "py": { "hashes": [ From e15fb5944f591920a02e94b1b0a45475e6a4e299 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 2 Nov 2020 15:42:58 +0100 Subject: [PATCH 320/333] Update publish-to-test-pypi.yml --- .github/workflows/publish-to-test-pypi.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index b3fec8647..0fe4b695e 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -1,16 +1,18 @@ name: Publish đŸ“Ļ to TestPyPI on: - create: + push: tags: - - '*' + - 'v*' jobs: build-n-publish: name: Build and publish đŸ“Ļ to TestPyPI runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 + with: + ref: development - name: Set up Python 3.7 uses: actions/setup-python@v2.1.4 with: @@ -25,5 +27,5 @@ jobs: - name: Publish đŸ“Ļ to Test PyPI uses: pypa/gh-action-pypi-publish@master with: - password: ${{ secrets.pypi_test_token }} + password: ${{ secrets.PYPI_TEST_TOKEN }} repository_url: https://test.pypi.org/legacy/ From c519ca5bfbae24ee15ba4631e1059e7bcf20f83d Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Mon, 2 Nov 2020 22:42:46 +0100 Subject: [PATCH 321/333] Update tools and deps Signed-off-by: Hugo D. (jabesq) --- .pre-commit-config.yaml | 6 +-- Pipfile.lock | 109 ++++++---------------------------------- 2 files changed, 18 insertions(+), 97 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 583cc8f91..c5849ecfe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: args: [--application-directories,./src] - repo: https://github.com/asottile/pyupgrade - rev: v2.7.2 + rev: v2.7.3 hooks: - id: pyupgrade args: [--py37-plus] @@ -56,7 +56,7 @@ repos: exclude: tests/ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 # Use the ref you want to point at + rev: v3.3.0 # Use the ref you want to point at hooks: - id: check-ast - id: no-commit-to-branch @@ -79,6 +79,6 @@ repos: additional_dependencies: [flake8-typing-imports==1.10.0] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.15.0 + rev: v1.15.1 hooks: - id: setup-cfg-fmt diff --git a/Pipfile.lock b/Pipfile.lock index c4d8b7dc3..b3f6e707a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -116,47 +116,6 @@ ], "version": "==2020.6.20" }, - "cffi": { - "hashes": [ - "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", - "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", - "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", - "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", - "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", - "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", - "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", - "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", - "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", - "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", - "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", - "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", - "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", - "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", - "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", - "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", - "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", - "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", - "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", - "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", - "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", - "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", - "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", - "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", - "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", - "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", - "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", - "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", - "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", - "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", - "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", - "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", - "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", - "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", - "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", - "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" - ], - "version": "==1.14.3" - }, "cfgv": { "hashes": [ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", @@ -224,33 +183,6 @@ ], "version": "==5.3" }, - "cryptography": { - "hashes": [ - "sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538", - "sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f", - "sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77", - "sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b", - "sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33", - "sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e", - "sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb", - "sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e", - "sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7", - "sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297", - "sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d", - "sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7", - "sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b", - "sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7", - "sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4", - "sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8", - "sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b", - "sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851", - "sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13", - "sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b", - "sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3", - "sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df" - ], - "version": "==3.2.1" - }, "distlib": { "hashes": [ "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", @@ -291,10 +223,10 @@ }, "identify": { "hashes": [ - "sha256:3139bf72d81dfd785b0a464e2776bd59bdc725b4cc10e6cf46b56a0db931c82e", - "sha256:969d844b7a85d32a5f9ac4e163df6e846d73c87c8b75847494ee8f4bd2186421" + "sha256:b505f7658afbddc11556de5ff9a5a52eed81b9a380f8ff77b7dd9781cfee6884", + "sha256:e3c822614168c9ac248f4258ffe43092debceb45e299a07b8f2fb168ba875605" ], - "version": "==1.5.6" + "version": "==1.5.7" }, "idna": { "hashes": [ @@ -326,14 +258,6 @@ "index": "pypi", "version": "==5.6.4" }, - "jeepney": { - "hashes": [ - "sha256:3479b861cc2b6407de5188695fa1a8d57e5072d7059322469b62628869b8e36e", - "sha256:d6c6b49683446d2407d2fe3acb7a368a77ff063f9182fe427da15d622adc24cf" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.4.3" - }, "keyring": { "hashes": [ "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", @@ -458,13 +382,6 @@ ], "version": "==2.6.0" }, - "pycparser": { - "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" - ], - "version": "==2.20" - }, "pyflakes": { "hashes": [ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", @@ -554,21 +471,33 @@ "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f", "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb", "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5", + "sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de", "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c", + "sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0", + "sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c", + "sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64", "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53", "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12", + "sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740", "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c", "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd", "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504", + "sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427", "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b", "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e", "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582", "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0", "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c", + "sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9", "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0", + "sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf", + "sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898", "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd", + "sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d", + "sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f", "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e", "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786", + "sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b", "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de", "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789", "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520", @@ -610,14 +539,6 @@ ], "version": "==1.4.0" }, - "secretstorage": { - "hashes": [ - "sha256:15da8a989b65498e29be338b3b279965f1b8f09b9668bd8010da183024c8bff6", - "sha256:b5ec909dde94d4ae2fa26af7c089036997030f0cf0a5cb372b4cccabd81c143b" - ], - "markers": "sys_platform == 'linux'", - "version": "==3.1.2" - }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", From 8fc1ac82da7a01aa747e4e2ed01b6a4ef2f5b936 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Mon, 2 Nov 2020 22:45:28 +0100 Subject: [PATCH 322/333] Bump version to v4.2.0 Signed-off-by: Hugo D. (jabesq) --- src/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.py b/src/version.py index b3c04842e..0dbba4d97 100644 --- a/src/version.py +++ b/src/version.py @@ -1,4 +1,4 @@ MAJOR_VERSION = 4 -MINOR_VERSION = 1 +MINOR_VERSION = 2 PATCH_VERSION = "0" __version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}" From 012bfb46e21bd556b48fd6bf91dbd5175d0c8e78 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Mon, 2 Nov 2020 23:23:18 +0100 Subject: [PATCH 323/333] Create release_gh.yml --- .github/workflows/release_gh.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/release_gh.yml diff --git a/.github/workflows/release_gh.yml b/.github/workflows/release_gh.yml new file mode 100644 index 000000000..aa5d2b5cf --- /dev/null +++ b/.github/workflows/release_gh.yml @@ -0,0 +1,30 @@ +# This is a basic workflow to help you get started with Actions + +name: Create Github Release + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the development branch +on: + push: + branches: [ master ] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + # Runs a single command using the runners shell + - name: Create a Release + uses: actions/create-release@v1.1.4 + with: + # The name of the tag. This should come from the webhook payload, `github.GITHUB_REF` when a user pushes a new tag + tag_name: ${{ github.ref }} + # The name of the release. For example, `Release v1.0.1` + release_name: Release ${{ github.ref }} From a5db7bfacc62519ac2017aafe4f65e3439babefc Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Mon, 2 Nov 2020 23:33:00 +0100 Subject: [PATCH 324/333] Update release_gh.yml --- .github/workflows/release_gh.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release_gh.yml b/.github/workflows/release_gh.yml index aa5d2b5cf..4af6c0765 100644 --- a/.github/workflows/release_gh.yml +++ b/.github/workflows/release_gh.yml @@ -6,7 +6,9 @@ name: Create Github Release # events but only for the development branch on: push: - branches: [ master ] + # Sequence of patterns matched against refs/tags + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: From 09c45e8e73ce351520e979ec31973c3f5b1b4c74 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Mon, 2 Nov 2020 23:38:28 +0100 Subject: [PATCH 325/333] Create codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 68 +++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..c3b3bdb48 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,68 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# ******** NOTE ******** + +name: "CodeQL" + +on: + push: + branches: [ development, master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ development ] + schedule: + - cron: '21 22 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 3a1c7688766fd6d05c935c36f152ab1b1ae0c1e4 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Mon, 2 Nov 2020 23:07:42 +0100 Subject: [PATCH 326/333] Release to pypi only on version change on master branch Signed-off-by: Hugo D. (jabesq) --- .github/workflows/publish-to-pypi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 399cac0cd..875a7e6e7 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -4,8 +4,8 @@ on: push: branches: - master - tags-ignore: - - "*.*b*" + paths: + - "**/version.py" jobs: build-n-publish: From e32258c188df154f8c23b9cc2388016e0a3ba889 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Thu, 12 Nov 2020 08:17:07 +0200 Subject: [PATCH 327/333] Update requests from 2.24.0 to 2.25.0 (#148) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 4fb919cb3..7f16906e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ packages = find: py_modules = version install_requires = oauthlib==3.1.0 - requests==2.24.0 + requests==2.25.0 requests_oauthlib==1.3.0 python_requires = >=3.7 package_dir = =src From f0691cfb1d1d4081d2f7d31ca725719ab22b2a3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 15 Nov 2020 08:17:25 +0100 Subject: [PATCH 328/333] Bump requests from 2.24.0 to 2.25.0 (#149) Bumps [requests](https://github.com/psf/requests) from 2.24.0 to 2.25.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.24.0...v2.25.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 243 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 168 insertions(+), 75 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index b3f6e707a..b96fab871 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,10 +18,10 @@ "default": { "certifi": { "hashes": [ - "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", - "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd", + "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4" ], - "version": "==2020.6.20" + "version": "==2020.11.8" }, "chardet": { "hashes": [ @@ -50,11 +50,11 @@ }, "requests": { "hashes": [ - "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", - "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", + "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998" ], "index": "pypi", - "version": "==2.24.0" + "version": "==2.25.0" }, "requests-oauthlib": { "hashes": [ @@ -66,10 +66,10 @@ }, "urllib3": { "hashes": [ - "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", - "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" + "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", + "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" ], - "version": "==1.25.11" + "version": "==1.26.2" } }, "develop": { @@ -89,10 +89,10 @@ }, "attrs": { "hashes": [ - "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", - "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "version": "==20.2.0" + "version": "==20.3.0" }, "black": { "hashes": [ @@ -111,10 +111,51 @@ }, "certifi": { "hashes": [ - "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", - "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" - ], - "version": "==2020.6.20" + "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd", + "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4" + ], + "version": "==2020.11.8" + }, + "cffi": { + "hashes": [ + "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", + "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", + "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", + "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", + "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", + "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", + "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", + "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", + "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", + "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", + "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", + "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", + "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", + "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", + "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", + "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", + "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", + "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", + "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", + "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", + "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", + "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", + "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", + "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", + "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", + "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", + "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", + "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", + "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", + "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", + "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", + "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", + "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", + "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", + "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", + "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" + ], + "version": "==1.14.3" }, "cfgv": { "hashes": [ @@ -183,6 +224,33 @@ ], "version": "==5.3" }, + "cryptography": { + "hashes": [ + "sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538", + "sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f", + "sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77", + "sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b", + "sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33", + "sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e", + "sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb", + "sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e", + "sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7", + "sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297", + "sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d", + "sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7", + "sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b", + "sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7", + "sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4", + "sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8", + "sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b", + "sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851", + "sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13", + "sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b", + "sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3", + "sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df" + ], + "version": "==3.2.1" + }, "distlib": { "hashes": [ "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", @@ -223,10 +291,10 @@ }, "identify": { "hashes": [ - "sha256:b505f7658afbddc11556de5ff9a5a52eed81b9a380f8ff77b7dd9781cfee6884", - "sha256:e3c822614168c9ac248f4258ffe43092debceb45e299a07b8f2fb168ba875605" + "sha256:5dd84ac64a9a115b8e0b27d1756b244b882ad264c3c423f42af8235a6e71ca12", + "sha256:c9504ba6a043ee2db0a9d69e43246bc138034895f6338d5aed1b41e4a73b1513" ], - "version": "==1.5.7" + "version": "==1.5.9" }, "idna": { "hashes": [ @@ -258,12 +326,20 @@ "index": "pypi", "version": "==5.6.4" }, + "jeepney": { + "hashes": [ + "sha256:2531d17ccfb3485d4eaee03c1a19a75f28b3ac0fbb5a1b683b77b820e5b0f509", + "sha256:e0e057fe2069a54257de32eb26cf14aac5fa90f5836f49926009a5022fb1e31a" + ], + "markers": "sys_platform == 'linux'", + "version": "==0.5.0" + }, "keyring": { "hashes": [ - "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", - "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466" + "sha256:12de23258a95f3b13e5b167f7a641a878e91eab8ef16fafc077720a95e6115bb", + "sha256:207bd66f2a9881c835dad653da04e196c678bf104f8252141d2d3c4f31051579" ], - "version": "==21.4.0" + "version": "==21.5.0" }, "lazy-object-proxy": { "hashes": [ @@ -341,10 +417,10 @@ }, "pathspec": { "hashes": [ - "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", - "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" + "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", + "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d" ], - "version": "==0.8.0" + "version": "==0.8.1" }, "pkginfo": { "hashes": [ @@ -382,6 +458,13 @@ ], "version": "==2.6.0" }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "version": "==2.20" + }, "pyflakes": { "hashes": [ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", @@ -406,10 +489,10 @@ }, "pyparsing": { "hashes": [ - "sha256:1060635ca5ac864c2b7bc7b05a448df4e32d7d8c65e33cbe1514810d339672a2", - "sha256:56a551039101858c9e189ac9e66e330a03fb7079e97ba6b50193643905f450ce" + "sha256:13140e8d0e1edd806eb50f18535d77f2143b40771d4aaef6b4950dd93d48a7db", + "sha256:38891c1032d0c759f0fa5ed3a8f249fd992b083fa2303ead58ee48a51b269e02" ], - "version": "==3.0.0a2" + "version": "==3.0.0b1" }, "pytest": { "hashes": [ @@ -467,55 +550,57 @@ }, "regex": { "hashes": [ - "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a", - "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f", - "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb", - "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5", - "sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de", - "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c", - "sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0", - "sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c", - "sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64", - "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53", - "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12", - "sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740", - "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c", - "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd", - "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504", - "sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427", - "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b", - "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e", - "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582", - "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0", - "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c", - "sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9", - "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0", - "sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf", - "sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898", - "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd", - "sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d", - "sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f", - "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e", - "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786", - "sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b", - "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de", - "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789", - "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520", - "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa", - "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b", - "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625", - "sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d", - "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26" - ], - "version": "==2020.10.28" + "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538", + "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4", + "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc", + "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa", + "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444", + "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1", + "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af", + "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8", + "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9", + "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88", + "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba", + "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364", + "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e", + "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7", + "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0", + "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31", + "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683", + "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee", + "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b", + "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884", + "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c", + "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e", + "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562", + "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85", + "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c", + "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6", + "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d", + "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b", + "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70", + "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b", + "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b", + "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f", + "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0", + "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5", + "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5", + "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f", + "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e", + "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512", + "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d", + "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917", + "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f" + ], + "version": "==2020.11.13" }, "requests": { "hashes": [ - "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", - "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", + "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998" ], "index": "pypi", - "version": "==2.24.0" + "version": "==2.25.0" }, "requests-mock": { "hashes": [ @@ -539,6 +624,14 @@ ], "version": "==1.4.0" }, + "secretstorage": { + "hashes": [ + "sha256:46305c3847ee3f7252b284e0eee5590fa6341c891104a2fd2313f8798c615a82", + "sha256:ed5279d788af258e4676fa26b6efb6d335a31f1f9f529b6f1e200f388fac33e1" + ], + "markers": "sys_platform == 'linux'", + "version": "==3.2.0" + }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", @@ -622,10 +715,10 @@ }, "urllib3": { "hashes": [ - "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", - "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" + "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", + "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" ], - "version": "==1.25.11" + "version": "==1.26.2" }, "virtualenv": { "hashes": [ From ed998264704965fcdec9aca4009bf26e53852287 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 09:47:52 +0100 Subject: [PATCH 329/333] Bump pre-commit from 2.8.2 to 2.9.0 (#153) Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.8.2 to 2.9.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/master/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.8.2...v2.9.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index b96fab871..d1688f88d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -328,11 +328,11 @@ }, "jeepney": { "hashes": [ - "sha256:2531d17ccfb3485d4eaee03c1a19a75f28b3ac0fbb5a1b683b77b820e5b0f509", - "sha256:e0e057fe2069a54257de32eb26cf14aac5fa90f5836f49926009a5022fb1e31a" + "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657", + "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae" ], "markers": "sys_platform == 'linux'", - "version": "==0.5.0" + "version": "==0.6.0" }, "keyring": { "hashes": [ @@ -438,11 +438,11 @@ }, "pre-commit": { "hashes": [ - "sha256:22e6aa3bd571debb01eb7d34483f11c01b65237be4eebbf30c3d4fb65762d315", - "sha256:905ebc9b534b991baec87e934431f2d0606ba27f2b90f7f652985f5a5b8b6ae6" + "sha256:4aee0db4808fa48d2458cedd5b9a084ef24dda1a0fa504432a11977a4d1cfd0a", + "sha256:b2d106d51c6ba6217e859d81774aae33fd825fe7de0dcf0c46e2586333d7a92e" ], "index": "pypi", - "version": "==2.8.2" + "version": "==2.9.0" }, "py": { "hashes": [ @@ -530,11 +530,13 @@ "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" @@ -656,10 +658,10 @@ }, "tqdm": { "hashes": [ - "sha256:9ad44aaf0fc3697c06f6e05c7cf025dd66bc7bcb7613c66d85f4464c47ac8fad", - "sha256:ef54779f1c09f346b2b5a8e5c61f96fbcb639929e640e59f8cf810794f406432" + "sha256:3d3f1470d26642e88bd3f73353cb6ff4c51ef7d5d7efef763238f4bc1f7e4e81", + "sha256:5ff3f5232b19fa4c5531641e480b7fad4598819f708a32eb815e6ea41c5fa313" ], - "version": "==4.51.0" + "version": "==4.53.0" }, "twine": { "hashes": [ @@ -722,10 +724,10 @@ }, "virtualenv": { "hashes": [ - "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", - "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" + "sha256:6af42359fbb33a6c7eab4d3246524b96fd9d8e07e7141b7a65998f96e28b2c57", + "sha256:fd4147c5ba3f694e2e4fc3c767407dc2226899623bb9b49c2f15637c2ee335b3" ], - "version": "==20.1.0" + "version": "==20.2.0" }, "webencodings": { "hashes": [ From 8b42aa0fb89e321d792cb87a21c4ced42fac222c Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 16 Nov 2020 15:31:59 +0100 Subject: [PATCH 330/333] Loosen the requirements --- setup.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 7f16906e6..9fbeaabc1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,9 +24,9 @@ classifiers = packages = find: py_modules = version install_requires = - oauthlib==3.1.0 - requests==2.25.0 - requests_oauthlib==1.3.0 + oauthlib>=3.1.0 + requests>=2.23.0 + requests_oauthlib>=1.3.0 python_requires = >=3.7 package_dir = =src From 1a75127835dcb14eaa92bbc8a37ccda83a8112c6 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 19 Nov 2020 08:57:23 +0100 Subject: [PATCH 331/333] Bump version --- src/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.py b/src/version.py index 0dbba4d97..a5cb1585c 100644 --- a/src/version.py +++ b/src/version.py @@ -1,4 +1,4 @@ MAJOR_VERSION = 4 MINOR_VERSION = 2 -PATCH_VERSION = "0" +PATCH_VERSION = 1 __version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}" From 0433f01f1967d50c53fbf6c2da5b79d157528dee Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 20 Nov 2020 22:58:36 +0100 Subject: [PATCH 332/333] Fix missing station modules --- fixtures/weatherstation_data_simple.json | 74 ++++++++++++++++++++++++ src/pyatmo/camera.py | 2 +- src/pyatmo/weather_station.py | 8 +-- tests/test_pyatmo_weatherstation.py | 41 +++++++++++++ 4 files changed, 118 insertions(+), 7 deletions(-) diff --git a/fixtures/weatherstation_data_simple.json b/fixtures/weatherstation_data_simple.json index 0d186405c..2a18c7bd2 100644 --- a/fixtures/weatherstation_data_simple.json +++ b/fixtures/weatherstation_data_simple.json @@ -505,6 +505,80 @@ "pressure_trend": "up" }, "modules": [] + }, + { + "_id": "12:34:56:58:c8:54", + "date_setup": 1605594014, + "last_setup": 1605594014, + "type": "NAMain", + "last_status_store": 1605878352, + "firmware": 178, + "wifi_status": 47, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 65, + "city": "Njurunda District", + "country": "SE", + "timezone": "Europe/Stockholm", + "location": [ + 17.123456, + 62.123456 + ] + }, + "station_name": "Njurunda (Indoor)", + "home_id": "5fb36b9ec68fd10c6467ca65", + "home_name": "Njurunda", + "dashboard_data": { + "time_utc": 1605878349, + "Temperature": 19.7, + "CO2": 993, + "Humidity": 40, + "Noise": 40, + "Pressure": 1015.6, + "AbsolutePressure": 1007.8, + "min_temp": 19.7, + "max_temp": 20.4, + "date_max_temp": 1605826917, + "date_min_temp": 1605873207, + "temp_trend": "stable", + "pressure_trend": "up" + }, + "modules": [ + { + "_id": "12:34:56:58:e6:38", + "type": "NAModule1", + "last_setup": 1605594034, + "data_type": [ + "Temperature", + "Humidity" + ], + "battery_percent": 100, + "reachable": true, + "firmware": 50, + "last_message": 1605878347, + "last_seen": 1605878328, + "rf_status": 62, + "battery_vp": 6198, + "dashboard_data": { + "time_utc": 1605878328, + "Temperature": 0.6, + "Humidity": 77, + "min_temp": -2.1, + "max_temp": 1.5, + "date_max_temp": 1605865920, + "date_min_temp": 1605826904, + "temp_trend": "down" + } + } + ] } ], "user": { diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index a8561fe4f..5d61a41bf 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -280,7 +280,7 @@ def get_event_id(data: Dict): "event_id": event_id, } - event_list = [] + event_list: List = [] resp: Optional[Dict[str, Any]] = None try: resp = self.auth.post_request(url=_GETEVENTSUNTIL_REQ, params=post_params) diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index 59c32d2cd..392ebe4a5 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -55,12 +55,8 @@ def __init__(self, auth: NetatmoOAuth2, url_req: str = None) -> None: item["modules"] = [item] for module in item["modules"]: - if "module_name" not in module: - if module["type"] == "NHC": - module["module_name"] = module["station_name"] - - else: - continue + if "module_name" not in module and module["type"] == "NHC": + module["module_name"] = module["station_name"] self.modules[module["_id"]] = module self.modules[module["_id"]]["main_device"] = item["_id"] diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index bdded0a96..78ac1e830 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -128,6 +128,21 @@ def test_weather_station_get_module_names(weather_station_data, station_id, expe }, }, ), + ( + "12:34:56:58:c8:54", + { + "12:34:56:58:c8:54": { + "id": "12:34:56:58:c8:54", + "module_name": "NAMain", + "station_name": "Njurunda (Indoor)", + }, + "12:34:56:58:e6:38": { + "id": "12:34:56:58:e6:38", + "module_name": "NAModule1", + "station_name": "Njurunda (Indoor)", + }, + }, + ), pytest.param( "NoValidStation", None, @@ -243,6 +258,32 @@ def test_weather_station_get_module(weather_station_data, mid, expected): "wifi_status", ], ), + ( + "12:34:56:58:c8:54", + [ + "CO2", + "Humidity", + "Noise", + "Pressure", + "Temperature", + "pressure_trend", + "reachable", + "temp_trend", + "wifi_status", + ], + ), + ( + "12:34:56:58:e6:38", + [ + "Humidity", + "Temperature", + "battery_percent", + "battery_vp", + "reachable", + "rf_status", + "temp_trend", + ], + ), pytest.param( None, None, From 23177fa9ebec39c415d9f7dd3ab82c4c9d9d6337 Mon Sep 17 00:00:00 2001 From: "Hugo D. (jabesq)" Date: Fri, 27 Nov 2020 21:43:35 +0100 Subject: [PATCH 333/333] tools: Update deps and tools Signed-off-by: Hugo D. (jabesq) --- .pre-commit-config.yaml | 2 +- Pipfile | 2 +- Pipfile.lock | 193 ++++++++++++++-------------------------- setup.cfg | 8 +- 4 files changed, 72 insertions(+), 133 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c5849ecfe..eb0336668 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: args: [--application-directories,./src] - repo: https://github.com/asottile/pyupgrade - rev: v2.7.3 + rev: v2.7.4 hooks: - id: pyupgrade args: [--py37-plus] diff --git a/Pipfile b/Pipfile index 1d438ffff..e3c6fb111 100644 --- a/Pipfile +++ b/Pipfile @@ -5,7 +5,7 @@ verify_ssl = true [dev-packages] black = "*" -bleach = ">=3.1.4" +bleach = "~=3.1.4" docutils = "*" flake8 = "*" freezegun = "*" diff --git a/Pipfile.lock b/Pipfile.lock index d1688f88d..2ddb03f05 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9f8f3df23b192e91113bc20807b39657119a4226864d039f8db144ebd0af2723" + "sha256": "04da7ba3b0557449cfc88503e8949a784f2d0040ed3eec3db8c6f9ba9c30ce61" }, "pipfile-spec": 6, "requires": { @@ -35,6 +35,7 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "oauthlib": { @@ -42,6 +43,7 @@ "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.1.0" }, "pyatmo": { @@ -50,26 +52,29 @@ }, "requests": { "hashes": [ - "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", - "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998" + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:5d2d0ffbb515f39417009a46c14256291061ac01ba8f875b90cad137de83beb4", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" ], "index": "pypi", - "version": "==2.25.0" + "version": "==2.23.0" }, "requests-oauthlib": { "hashes": [ "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", - "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" + "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", + "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" ], "index": "pypi", "version": "==1.3.0" }, "urllib3": { "hashes": [ - "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", - "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" + "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", + "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], - "version": "==1.26.2" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.11" } }, "develop": { @@ -85,6 +90,7 @@ "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], + "markers": "python_version >= '3.5'", "version": "==2.4.2" }, "attrs": { @@ -92,6 +98,7 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "black": { @@ -103,11 +110,11 @@ }, "bleach": { "hashes": [ - "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", - "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" + "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f", + "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b" ], "index": "pypi", - "version": "==3.2.1" + "version": "==3.1.5" }, "certifi": { "hashes": [ @@ -116,52 +123,12 @@ ], "version": "==2020.11.8" }, - "cffi": { - "hashes": [ - "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", - "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", - "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", - "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", - "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", - "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", - "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", - "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", - "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", - "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", - "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", - "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", - "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", - "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", - "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", - "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", - "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", - "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", - "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", - "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", - "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", - "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", - "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", - "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", - "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", - "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", - "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", - "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", - "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", - "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", - "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", - "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", - "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", - "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", - "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", - "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" - ], - "version": "==1.14.3" - }, "cfgv": { "hashes": [ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], + "markers": "python_full_version >= '3.6.1'", "version": "==3.2.0" }, "chardet": { @@ -176,6 +143,7 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "colorama": { @@ -183,6 +151,7 @@ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.4.4" }, "coverage": { @@ -222,35 +191,9 @@ "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==5.3" }, - "cryptography": { - "hashes": [ - "sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538", - "sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f", - "sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77", - "sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b", - "sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33", - "sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e", - "sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb", - "sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e", - "sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7", - "sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297", - "sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d", - "sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7", - "sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b", - "sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7", - "sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4", - "sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8", - "sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b", - "sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851", - "sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13", - "sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b", - "sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3", - "sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df" - ], - "version": "==3.2.1" - }, "distlib": { "hashes": [ "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", @@ -291,25 +234,27 @@ }, "identify": { "hashes": [ - "sha256:5dd84ac64a9a115b8e0b27d1756b244b882ad264c3c423f42af8235a6e71ca12", - "sha256:c9504ba6a043ee2db0a9d69e43246bc138034895f6338d5aed1b41e4a73b1513" + "sha256:943cd299ac7f5715fcb3f684e2fc1594c1e0f22a90d15398e5888143bd4144b5", + "sha256:cc86e6a9a390879dcc2976cef169dd9cc48843ed70b7380f321d1b118163c60e" ], - "version": "==1.5.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.5.10" }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "importlib-metadata": { "hashes": [ - "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da", - "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3" + "sha256:030f3b1bdb823ecbe4a9659e14cc861ce5af403fe99863bae173ec5fe00ab132", + "sha256:caeee3603f5dcf567864d1be9b839b0bcfdf1383e3e7be33ce2dead8144ff19c" ], "markers": "python_version < '3.8'", - "version": "==2.0.0" + "version": "==2.1.0" }, "iniconfig": { "hashes": [ @@ -326,19 +271,12 @@ "index": "pypi", "version": "==5.6.4" }, - "jeepney": { - "hashes": [ - "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657", - "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.6.0" - }, "keyring": { "hashes": [ "sha256:12de23258a95f3b13e5b167f7a641a878e91eab8ef16fafc077720a95e6115bb", "sha256:207bd66f2a9881c835dad653da04e196c678bf104f8252141d2d3c4f31051579" ], + "markers": "python_version >= '3.6'", "version": "==21.5.0" }, "lazy-object-proxy": { @@ -365,6 +303,7 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -413,6 +352,7 @@ "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.4" }, "pathspec": { @@ -434,21 +374,23 @@ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.13.1" }, "pre-commit": { "hashes": [ - "sha256:4aee0db4808fa48d2458cedd5b9a084ef24dda1a0fa504432a11977a4d1cfd0a", - "sha256:b2d106d51c6ba6217e859d81774aae33fd825fe7de0dcf0c46e2586333d7a92e" + "sha256:949b13efb7467ae27e2c8f9e83434dacf2682595124d8902554a4e18351e5781", + "sha256:e31c04bc23741194a7c0b983fe512801e151a0638c6001c49f2bd034f8a664a1" ], "index": "pypi", - "version": "==2.9.0" + "version": "==2.9.2" }, "py": { "hashes": [ "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.9.0" }, "pycodestyle": { @@ -456,20 +398,15 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, - "pycparser": { - "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" - ], - "version": "==2.20" - }, "pyflakes": { "hashes": [ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pygments": { @@ -477,6 +414,7 @@ "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0", "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773" ], + "markers": "python_version >= '3.5'", "version": "==2.7.2" }, "pylint": { @@ -489,10 +427,11 @@ }, "pyparsing": { "hashes": [ - "sha256:13140e8d0e1edd806eb50f18535d77f2143b40771d4aaef6b4950dd93d48a7db", - "sha256:38891c1032d0c759f0fa5ed3a8f249fd992b083fa2303ead58ee48a51b269e02" + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "version": "==3.0.0b1" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.7" }, "pytest": { "hashes": [ @@ -523,6 +462,7 @@ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.1" }, "pyyaml": { @@ -598,11 +538,12 @@ }, "requests": { "hashes": [ - "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", - "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998" + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:5d2d0ffbb515f39417009a46c14256291061ac01ba8f875b90cad137de83beb4", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" ], "index": "pypi", - "version": "==2.25.0" + "version": "==2.23.0" }, "requests-mock": { "hashes": [ @@ -626,19 +567,12 @@ ], "version": "==1.4.0" }, - "secretstorage": { - "hashes": [ - "sha256:46305c3847ee3f7252b284e0eee5590fa6341c891104a2fd2313f8798c615a82", - "sha256:ed5279d788af258e4676fa26b6efb6d335a31f1f9f529b6f1e200f388fac33e1" - ], - "markers": "sys_platform == 'linux'", - "version": "==3.2.0" - }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "toml": { @@ -646,6 +580,7 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "tox": { @@ -658,10 +593,11 @@ }, "tqdm": { "hashes": [ - "sha256:3d3f1470d26642e88bd3f73353cb6ff4c51ef7d5d7efef763238f4bc1f7e4e81", - "sha256:5ff3f5232b19fa4c5531641e480b7fad4598819f708a32eb815e6ea41c5fa313" + "sha256:5c0d04e06ccc0da1bd3fa5ae4550effcce42fcad947b4a6cafa77bdc9b09ff22", + "sha256:9e7b8ab0ecbdbf0595adadd5f0ebbb9e69010e0bd48bbb0c15e550bf2a5292df" ], - "version": "==4.53.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==4.54.0" }, "twine": { "hashes": [ @@ -704,7 +640,7 @@ "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], - "markers": "implementation_name == 'cpython' and python_version < '3.8'", + "markers": "python_version < '3.8' and implementation_name == 'cpython'", "version": "==1.4.1" }, "typing-extensions": { @@ -717,17 +653,19 @@ }, "urllib3": { "hashes": [ - "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", - "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" + "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", + "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], - "version": "==1.26.2" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.11" }, "virtualenv": { "hashes": [ - "sha256:6af42359fbb33a6c7eab4d3246524b96fd9d8e07e7141b7a65998f96e28b2c57", - "sha256:fd4147c5ba3f694e2e4fc3c767407dc2226899623bb9b49c2f15637c2ee335b3" + "sha256:07cff122e9d343140366055f31be4dcd61fd598c69d11cd33a9d9c8df4546dd7", + "sha256:e0aac7525e880a429764cefd3aaaff54afb5d9f25c82627563603f5d7de5a6e5" ], - "version": "==20.2.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.1" }, "webencodings": { "hashes": [ @@ -747,6 +685,7 @@ "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108", "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb" ], + "markers": "python_version >= '3.6'", "version": "==3.4.0" } } diff --git a/setup.cfg b/setup.cfg index 9fbeaabc1..b9a6a9f21 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,10 +24,10 @@ classifiers = packages = find: py_modules = version install_requires = - oauthlib>=3.1.0 - requests>=2.23.0 - requests_oauthlib>=1.3.0 -python_requires = >=3.7 + oauthlib~=3.1.0 + requests~=2.23.0 + requests_oauthlib~=1.3.0 +python_requires = ~=3.7 package_dir = =src [options.packages.find]