Skip to content

updated Thermostat and Homecoach #90

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Nov 23, 2024
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
Loading