Skip to content

Commit

Permalink
Merge pull request #90 from JurgenLB/master
Browse files Browse the repository at this point in the history
updated Thermostat and Homecoach
  • Loading branch information
philippelt authored Nov 23, 2024
2 parents 47bc528 + 009231b commit caf8058
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 68 deletions.
6 changes: 5 additions & 1 deletion .netatmo.credentials
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"USER_MAIL" : "",
"USER_PASSWORD" : "",
"CLIENT_ID" : "",
"CLIENT_SECRET" : "",
"REFRESH_TOKEN" : ""
"ACCESS_TOKEN" : "",
"REFRESH_TOKEN" : "",
"SCOPE" : ""
}

212 changes: 145 additions & 67 deletions lnetatmo.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Multiple contributors : see https://github.com/philippelt/netatmo-api-python
# License : GPL V3
"""
This API provides access to the Netatmo weather station or/and the Welcome camera
This API provides access to the Netatmo weather station or/and other installed devices
This package can be used with Python2 or Python3 applications and do not
require anything else than standard libraries
Expand Down Expand Up @@ -121,8 +121,8 @@
'NAModule2' : ["wind unit", 'Weather'],
'NAModule3' : ["rain unit", 'Weather'],
'NAModule4' : ["indoor unit", 'Weather'],
'NAPlug' : ["thermostat relais station", 'Energy'], # A smart thermostat exist of a thermostat and a Relais module
# The relais module is also the bridge for thermostat and Valves
'NAPlug' : ["thermostat relais station", 'Energy'], # A smart thermostat exist of a thermostat module and a Relay device
# The relay device is also the bridge for thermostat and Valves
'NATherm1' : ["thermostat", 'Energy'],
'NCO' : ["co2 sensor", 'Home + Security'], # The same API as smoke sensor
'NDB' : ["doorbell", 'Home + Security'],
Expand All @@ -131,6 +131,7 @@
'NSD' : ["smoke sensor", 'Home + Security'],
'NHC' : ["home coach", 'Aircare'],
'NIS' : ["indoor sirene", 'Home + Security'],
'NDL' : ["Doorlock", 'Home + Security'],

'NLC' : ["Cable Outlet", 'Home+Control'],
'NLE' : ["Ecometer", 'Home+Control'],
Expand Down Expand Up @@ -231,9 +232,9 @@ def __init__(self, clientId=None,

self._clientId = clientId or cred["CLIENT_ID"]
self._clientSecret = clientSecret or cred["CLIENT_SECRET"]
self._accessToken = None #accessToken or cred["ACCESS_TOKEN"] # Will be refreshed before any use
self.refreshToken = refreshToken or cred["REFRESH_TOKEN"]
self.expiration = 0 # Force refresh token
self._accessToken = None # Will be refreshed before any use

@property
def accessToken(self):
Expand Down Expand Up @@ -341,8 +342,8 @@ def getModuleParam(self, module_id, param):

class ThermostatData:
"""
List the Thermostat and temperature modules
List the Relay station and Thermostat modules
Valves are controlled by HomesData and HomeStatus in new API
Args:
authData (clientAuth): Authentication information with a working access Token
home : Home name or id of the home who's thermostat belongs to
Expand Down Expand Up @@ -375,36 +376,72 @@ def __init__(self, authData, home=None):
# Standard the first Relaystation and Thermostat is returned
# self.rawData is list all stations

# FIXME : This code is wrong as it will always return the first Relay
# I don't own a thermostat I can't fix this code, help welcome
def Relay_Plug(self, _id=None):
# if no ID is given the Relaystation at index 0 is returned
def Relay_Plug(self, Rid=""):
for Relay in self.rawData:
if _id in Relay:
return Relay
else:
if Rid in Relay['_id']:
print ('Relay ', Rid, 'in rawData')
#print (Relay.keys())
#print (Relay['_id'])
return Relay
#dict_keys(['_id', 'applications', 'cipher_id', 'command', 'config_version', 'd_amount', 'date_creation', 'dev_has_init', 'device_group', 'firmware', 'firmware_private', 'homekit_nb_pairing', 'last_bilan', 'last_day_extremum', 'last_fw_update', 'last_measure_stored', 'last_setup', 'last_status_store', 'last_sync_asked', 'last_time_boiler_on', 'mg_station_name', 'migration_date', 'module_history', 'netcom_transport', 'new_historic_data', 'place', 'plug_connected_boiler', 'recompute_outdoor_time', 'record_storage', 'rf_amb_status', 'setpoint_order_history', 'skip_module_history_creation', 'subtype', 'type', 'u_amount', 'update_device', 'upgrade_record_ts', 'wifi_status', 'room', 'modules', 'station_name', 'udp_conn', 'last_plug_seen'])

# FIXME : Probably wrong again, always returning "first" thermostat ?
def Thermostat_Data(self):
for thermostat in self.Relay_Plug()['modules']:
#
return thermostat
# if no ID is given the Thermostatmodule at index 0 is returned
def Thermostat_Data(self, tid=""):
for Relay in self.rawData:
for thermostat in Relay['modules']:
if tid in thermostat['_id']:
print ('Thermostat ',tid, 'in Relay', Relay['_id'], Relay['station_name'])
#print (thermostat['_id'])
#print (thermostat.keys())
return thermostat
#dict_keys(['_id', 'module_name', 'type', 'firmware', 'last_message', 'rf_status', 'battery_vp', 'therm_orientation', 'therm_relay_cmd', 'anticipating', 'battery_percent', 'event_history', 'last_therm_seen', 'setpoint', 'therm_program_list', 'measured'])

def getThermostat(self, name=None, tid=None):
if self.rawData[0]['station_name'] != name: return None # OLD ['name']
# FIXME: No thermostat property !!
return self.thermostat[self.defaultThermostatId]

def getThermostat(self, name=None, id=""):
for Relay in self.rawData:
for module in Relay['modules']:
if id == Relay['_id']:
print ('Relay ', id, 'found')
return Relay
elif name == Relay['station_name']:
print ('Relay ', name, 'found')
return Relay
elif id == module['_id']:
print ('Thermostat ', id, 'found in Relay', Relay['_id'], Relay['station_name'])
return module
elif name == module['module_name']:
print ('Thermostat ', name, 'found in Relay', Relay['_id'], Relay['station_name'])
return module
else:
#print ('Device NOT Found')
pass

def moduleNamesList(self, name=None, tid=None):
thermostat = self.getThermostat(name=name, tid=tid)
return [m['name'] for m in thermostat['modules']] if thermostat else None
l = []
for Relay in self.rawData:
if id == Relay['_id'] or name == Relay['station_name']:
RL = []
for module in Relay['modules']:
RL.append(module['module_name'])
return RL
else:
#print ("Cloud Data")
for module in Relay['modules']:
l.append(module['module_name'])
#This return a list off all connected Thermostat in the cloud.
return l

def getModuleByName(self, name, thermostatId=None): # ERROR 'NoneType' object is not subscriptable
thermostat = self.getThermostat(tid=thermostatId)
for m in thermostat['modules']:
if m['name'] == name: return m
return None
def getModuleByName(self, name, tid=""):
for Relay in self.rawData:
for module in Relay['modules']:
#print (module['module_name'], module['_id'])
if module['module_name'] == name:
return module
elif module['_id'] == tid:
return module
else:
pass


class WeatherStationData:
Expand Down Expand Up @@ -595,31 +632,48 @@ class HomeData:
Args:
authData (ClientAuth): Authentication information with a working access Token
home : Home name of the home where's devices are installed
"""
def __init__(self, authData, home=None):
warnings.warn("The 'HomeData' class is deprecated'",
DeprecationWarning )
self.getAuthToken = authData.accessToken
postParams = {
"access_token" : self.getAuthToken
}
resp = postRequest("Home data", _GETHOMEDATA_REQ, postParams)
self.rawData = resp['body']
# Collect homes
self.homes = { d['id'] : d for d in self.rawData['homes'] }
# FIXME : Doesn't use the home parameter to select the appropriate home !
for k, v in self.homes.items():
self.homeid = k
C = v.get('cameras')
P = v.get('persons')
S = v.get('smokedetectors')
E = v.get('events')
S or logger.warning('No smoke detector found')
C or logger.warning('No Cameras found')
P or logger.warning('No Persons found')
E or logger.warning('No events found')
if not (C or P or S or E):
raise NoDevice("No device found in home %s" % k)
self.homes = self.rawData['homes'][0]
for d in self.rawData['homes'] :
if home == d['name']:
self.homes = d
else:
pass
#
#print (self.homes.keys())
#dict_keys(['id', 'name', 'persons', 'place', 'cameras', 'smokedetectors', 'events'])
self.homeid = self.homes['id']
C = self.homes['cameras']
P = self.homes['persons']
S = self.homes['smokedetectors']
E = None
# events not always in self.homes
if 'events' in self.homes.keys():
E = self.homes['events']
#
if not S:
logger.warning('No smoke detector found')
if not C:
logger.warning('No Cameras found')
if not P:
logger.warning('No Persons found')
if not E:
logger.warning('No events found')
# if not (C or P or S or E):
# raise NoDevice("No device found in home %s" % k)
if S or C or P or E:
self.default_home = home or list(self.homes.values())[0]['name']
self.default_home = home or self.homes['name']
# Split homes data by category
self.persons = {}
self.events = {}
Expand All @@ -644,6 +698,7 @@ def __init__(self, authData, home=None):
c["home_id"] = curHome['id']
for camera,e in self.events.items():
self.lastEvent[camera] = e[sorted(e)[-1]]
#self.default_home has no key homeId use homeName instead!
if not self.cameras[self.default_home] : raise NoDevice("No camera available in default home")
self.default_camera = list(self.cameras[self.default_home].values())[0]
else:
Expand Down Expand Up @@ -921,6 +976,9 @@ def __init__(self, authData, home=None):
#print (h.keys())
if home in (h["name"], h["id"]):
self.Homes_Data = h
else:
self.Homes_Data = self.rawData[0]
self.homeid = self.Homes_Data['id']
if not self.Homes_Data : raise NoDevice("No Devices available")


Expand All @@ -932,10 +990,10 @@ class HomeCoach:
authData (clientAuth): Authentication information with a working access Token
home : Home name or id of the home who's HomeCoach belongs to
"""
# FIXME: home parameter not used, unpredictible behavior
def __init__(self, authData, home=None):
# I don't own a HomeCoach thus I am not able to test the HomeCoach support

def __init__(self, authData):
# I don't own a HomeCoach thus I am not able to test the HomeCoach support
# Homecoach does not need or use HomeID parameter
# warnings.warn("The HomeCoach code is not tested due to the lack of test environment.\n", RuntimeWarning )
# "As Netatmo is continuously breaking API compatibility, risk that current bindings are wrong is high.\n" \
# "Please report found issues (https://github.com/philippelt/netatmo-api-python/issues)"
Expand All @@ -949,34 +1007,39 @@ def __init__(self, authData, home=None):
# homecoach data
if not self.rawData : raise NoDevice("No HomeCoach available")

for h in self.rawData:
# FIXME: This loop is nonsense (always end with the last value)
self.HomecoachDevice = h
# print ('Homecoach = ', self.HomecoachDevice)
# print (' ')
# print ('Homecoach_data = ', self.rawData[i]['dashboard_data'])
# print (' ')
def HomecoachDevice(self, hid=""):
for device in self.rawData:
if hid == device['_id']:
return device
return None

def Dashboard(self):
D = self.HomecoachDevice['dashboard_data']
return D
def Dashboard(self, hid=""):
#D = self.HomecoachDevice['dashboard_data']
for device in self.rawData:
if hid == device['_id']:
D = device['dashboard_data']
return D

# FIXME: Exclusion of outdated info is not handled (exclude parameter unused)
def lastData(self, hid=None, exclude=0):
if hid is not None:
s = self.HomecoachDevice['dashboard_data']['time_utc']
_id = self.HomecoachDevice[hid]
return {'When':s}, {'_id':_id}
return {'When': 0 }, {'_id': hid}
for device in self.rawData:
if hid == device['_id']:
# LastData in HomeCoach
#s = self.HomecoachDevice['dashboard_data']['time_utc']
# Define oldest acceptable sensor measure event
limit = (time.time() - exclude) if exclude else 0
ds = device['dashboard_data']['time_utc']
return { '_id': hid, 'When': ds if device.get('time_utc',limit+10) > limit else 0}
else:
pass

def checkNotUpdated(self, res, _id, delay=3600):
def checkNotUpdated(self, res, hid, delay=3600):
ret = []
if time.time()-res['When'] > delay : ret.append({_id: 'Device Not Updated'})
if time.time()-res['When'] > delay : ret.append({hid: 'Device Not Updated'})
return ret if ret else None

def checkUpdated(self, res, _id, delay=3600):
def checkUpdated(self, res, hid, delay=3600):
ret = []
if time.time()-res['When'] < delay : ret.append({_id: 'Device up-to-date'})
if time.time()-res['When'] < delay : ret.append({hid: 'Device up-to-date'})
return ret if ret else None


Expand Down Expand Up @@ -1099,23 +1162,38 @@ def getStationMinMaxTH(station=None, module=None, home=None):

try:
homes = HomeData(authorization)
homeid = homes.homeid
except NoDevice :
logger.warning("No home available for testing")

try:
thermostat = ThermostatData(authorization)
Default_relay = thermostat.Relay_Plug()
Default_thermostat = thermostat.Thermostat_Data()
thermostat.getThermostat()
print (thermostat.moduleNamesList())
#print (thermostat.getModuleByName(name))
except NoDevice:
logger.warning("No thermostat avaible for testing")

try:
print (' ')
logger.info("Homes Data")
#homesdata = HomesData(authorization, homeid)
homesdata = HomesData(authorization)
homeid = homesdata.homeid
except NoDevice:
logger.warning("No HomesData avaible for testing")

try:
print (' ')
logger.info("Home Status")
HomeStatus(authorization, homeid)
except NoDevice:
logger.warning("No Home available for testing")

try:
print (' ')
logger.info("HomeCoach")
Homecoach = HomeCoach(authorization)
except NoDevice:
logger.warning("No HomeCoach avaible for testing")
Expand Down

0 comments on commit caf8058

Please sign in to comment.