@@ -81,6 +81,37 @@ def getParameter(key, default):
8181_GETEVENTSUNTIL_REQ = _BASE_URL + "api/geteventsuntil"
8282
8383
84+ #TODO# Undocumented (but would be very usefull) API : Access currently forbidden (403)
85+
86+ # _POST_UPDATE_HOME_REQ = _BASE_URL + "/api/updatehome"
87+
88+ # For presence setting (POST BODY):
89+ # _PRES_BODY_REC_SET = "home_id=%s&presence_settings[presence_record_%s]=%s" # (HomeId, DetectionKind, DetectionSetup.index)
90+ _PRES_DETECTION_KIND = ("humans" , "animals" , "vehicles" , "movements" )
91+ _PRES_DETECTION_SETUP = ("ignore" , "record" , "record & notify" )
92+
93+ # _PRES_BODY_ALERT_TIME = "home_id=%s&presence_settings[presence_notify_%s]=%s" # (HomeID, "from"|"to", "hh:mm")
94+
95+ # Regular (documented) commands (both cameras)
96+
97+ _PRES_CDE_GET_SNAP = "/live/snapshot_720.jpg"
98+
99+ #TODO# Undocumented (taken from https://github.com/KiboOst/php-NetatmoCameraAPI/blob/master/class/NetatmoCameraAPI.php)
100+ # Work with local_url only (undocumented scope control probably)
101+
102+ # For Presence camera
103+
104+ _PRES_CDE_GET_LIGHT = "/command/floodlight_get_config"
105+ # Not working yet, probably due to scope restriction
106+ #_PRES_CDE_SET_LIGHT = "/command/floodlight_set_config?config=mode:%s" # "auto"|"on"|"off"
107+
108+
109+ # For all cameras
110+
111+ _CAM_CHANGE_STATUS = "/command/changestatus?status=%s" # "on"|"off"
112+ # Not working yet
113+ #_CAM_FTP_ACTIVE = "/command/ftp_set_config?config=on_off:%s" # "on"|"off"
114+
84115
85116class NoDevice ( Exception ):
86117 pass
@@ -106,7 +137,7 @@ def __init__(self, clientId=_CLIENT_ID,
106137 clientSecret = _CLIENT_SECRET ,
107138 username = _USERNAME ,
108139 password = _PASSWORD ,
109- scope = "read_station read_camera access_camera read_presence access_presence" ):
140+ scope = "read_station read_camera access_camera write_camera read_presence access_presence write_presence " ):
110141
111142 postParams = {
112143 "grant_type" : "password" ,
@@ -325,27 +356,31 @@ def __init__(self, authData):
325356 }
326357 resp = postRequest (_GETHOMEDATA_REQ , postParams )
327358 self .rawData = resp ['body' ]
359+ # Collect homes
328360 self .homes = { d ['id' ] : d for d in self .rawData ['homes' ] }
329361 if not self .homes : raise NoDevice ("No home available" )
362+ self .default_home = list (self .homes .values ())[0 ]['name' ]
363+ # Split homes data by category
330364 self .persons = dict ()
331365 self .events = dict ()
332366 self .cameras = dict ()
333367 self .lastEvent = dict ()
334368 for i in range (len (self .rawData ['homes' ])):
335- nameHome = self .rawData ['homes' ][i ]['name' ]
369+ curHome = self .rawData ['homes' ][i ]
370+ nameHome = curHome ['name' ]
336371 if nameHome not in self .cameras :
337- self .cameras [nameHome ]= dict ()
338- for p in self . rawData [ 'homes' ][ i ] ['persons' ]:
372+ self .cameras [nameHome ] = dict ()
373+ for p in curHome ['persons' ]:
339374 self .persons [ p ['id' ] ] = p
340- for e in self . rawData [ 'homes' ][ i ] ['events' ]:
375+ for e in curHome ['events' ]:
341376 if e ['camera_id' ] not in self .events :
342377 self .events [ e ['camera_id' ] ] = dict ()
343378 self .events [ e ['camera_id' ] ][ e ['time' ] ] = e
344- for c in self . rawData [ 'homes' ][ i ] ['cameras' ]:
379+ for c in curHome ['cameras' ]:
345380 self .cameras [nameHome ][ c ['id' ] ] = c
381+ c ["home_id" ] = curHome ['id' ]
346382 for camera in self .events :
347- self .lastEvent [camera ]= self .events [camera ][sorted (self .events [camera ])[- 1 ]]
348- self .default_home = list (self .homes .values ())[0 ]['name' ]
383+ self .lastEvent [camera ] = self .events [camera ][sorted (self .events [camera ])[- 1 ]]
349384 if not self .cameras [self .default_home ] : raise NoDevice ("No camera available in default home" )
350385 self .default_camera = list (self .cameras [self .default_home ].values ())[0 ]
351386
@@ -386,6 +421,8 @@ def cameraUrls(self, camera=None, home=None, cid=None):
386421 """
387422 Return the vpn_url and the local_url (if available) of a given camera
388423 in order to access to its live feed
424+ Can't use the is_local property which is mostly false in case of operator
425+ dynamic IP change after presence start sequence
389426 """
390427 local_url = None
391428 vpn_url = None
@@ -395,13 +432,18 @@ def cameraUrls(self, camera=None, home=None, cid=None):
395432 camera_data = self .cameraByName (camera = camera , home = home )
396433 if camera_data :
397434 vpn_url = camera_data ['vpn_url' ]
398- resp = postRequest ('{0} /command/ping'. format ( camera_data [ 'vpn_url' ]), dict () )
435+ resp = postRequest (vpn_url + ' /command/ping' )
399436 temp_local_url = resp ['local_url' ]
400- resp = postRequest ('{0} /command/ping'. format ( temp_local_url ), dict () )
401- if temp_local_url == resp ['local_url' ]:
437+ resp = postRequest (temp_local_url + ' /command/ping', timeout = 1 )
438+ if resp and temp_local_url == resp ['local_url' ]:
402439 local_url = temp_local_url
403440 return vpn_url , local_url
404441
442+ def url (self , camera = None , home = None , cid = None ):
443+ vpn_url , local_url = self .cameraUrls (camera , home , cid )
444+ # Return local if available else vpn
445+ return local_url or vpn_url
446+
405447 def personsAtHome (self , home = None ):
406448 """
407449 Return the list of known persons who are currently at home
@@ -533,6 +575,47 @@ def motionDetected(self, home=None, camera=None):
533575 return True
534576 return False
535577
578+ def presenceUrl (self , camera = None , home = None , cid = None , setting = None ):
579+ camera = self .cameraByName (home = home , camera = camera ) or self .cameraById (cid = cid )
580+ if camera ["type" ] != "NOC" : return None # Not a presence camera
581+ vpnUrl , localUrl = self .cameraUrls (cid = camera ["id" ])
582+ return localUrl
583+
584+ def presenceLight (self , camera = None , home = None , cid = None , setting = None ):
585+ url = self .presenceUrl (home = home , camera = camera ) or self .cameraById (cid = cid )
586+ if not url or setting not in ("on" , "off" , "auto" ): return None
587+ if setting : return "Currently unsupported"
588+ return cameraCommand (url , _PRES_CDE_GET_LIGHT )["mode" ]
589+ # Not yet supported
590+ #if not setting: return cameraCommand(url, _PRES_CDE_GET_LIGHT)["mode"]
591+ #else: return cameraCommand(url, _PRES_CDE_SET_LIGHT, setting)
592+
593+ def presenceStatus (self , mode , camera = None , home = None , cid = None ):
594+ url = self .presenceUrl (home = home , camera = camera ) or self .cameraById (cid = cid )
595+ if not url or mode not in ("on" , "off" ) : return None
596+ r = cameraCommand (url , _CAM_CHANGE_STATUS , mode )
597+ return mode if r ["status" ] == "ok" else None
598+
599+ def presenceSetAction (self , camera = None , home = None , cid = None ,
600+ eventType = _PRES_DETECTION_KIND [0 ], action = 2 ):
601+ return "Currently unsupported"
602+ if eventType not in _PRES_DETECTION_KIND or \
603+ action not in _PRES_DETECTION_SETUP : return None
604+ camera = self .cameraByName (home = home , camera = camera ) or self .cameraById (cid = cid )
605+ postParams = { "access_token" : self .getAuthToken ,
606+ "home_id" : camera ["home_id" ],
607+ "presence_settings[presence_record_%s]" % eventType : _PRES_DETECTION_SETUP .index (action )
608+ }
609+ resp = postRequest (_POST_UPDATE_HOME_REQ , postParams )
610+ self .rawData = resp ['body' ]
611+
612+ def getLiveSnapshot (self , camera = None , home = None , cid = None ):
613+ camera = self .cameraByName (home = home , camera = camera ) or self .cameraById (cid = cid )
614+ vpnUrl , localUrl = self .cameraUrls (cid = camera ["id" ])
615+ url = localUrl or vpnUrl
616+ return cameraCommand (url , _PRES_CDE_GET_SNAP )
617+
618+
536619class WelcomeData (HomeData ):
537620 """
538621 This class is now deprecated. Use HomeData instead
@@ -544,17 +627,29 @@ class WelcomeData(HomeData):
544627
545628# Utilities routines
546629
547- def postRequest (url , params ):
630+ def cameraCommand (cameraUrl , commande , parameters = None , timeout = 3 ):
631+ url = cameraUrl + ( commande % parameters if parameters else commande )
632+ return postRequest (url , timeout = timeout )
633+
634+ def postRequest (url , params = None , timeout = 10 ):
548635 if version_info .major == 3 :
549636 req = urllib .request .Request (url )
550- req .add_header ("Content-Type" ,"application/x-www-form-urlencoded;charset=utf-8" )
551- params = urllib .parse .urlencode (params ).encode ('utf-8' )
552- resp = urllib .request .urlopen (req , params )
637+ if params :
638+ req .add_header ("Content-Type" ,"application/x-www-form-urlencoded;charset=utf-8" )
639+ params = urllib .parse .urlencode (params ).encode ('utf-8' )
640+ try :
641+ resp = urllib .request .urlopen (req , params , timeout = timeout ) if params else urllib .request .urlopen (req , timeout = timeout )
642+ except urllib .error .URLError :
643+ return None
553644 else :
554- params = urlencode (params )
555- headers = {"Content-Type" : "application/x-www-form-urlencoded;charset=utf-8" }
556- req = urllib2 .Request (url = url , data = params , headers = headers )
557- resp = urllib2 .urlopen (req )
645+ if params :
646+ params = urlencode (params )
647+ headers = {"Content-Type" : "application/x-www-form-urlencoded;charset=utf-8" }
648+ req = urllib2 .Request (url = url , data = params , headers = headers ) if params else urllib2 .Request (url )
649+ try :
650+ resp = urllib2 .urlopen (req , timeout = timeout )
651+ except urllib2 .URLError :
652+ return None
558653 data = b""
559654 for buff in iter (lambda : resp .read (65535 ), b'' ): data += buff
560655 # Return values in bytes if not json data to handle properly camera images
@@ -618,7 +713,8 @@ def getStationMinMaxTH(station=None, module=None):
618713
619714
620715 try :
621- Homes = HomeData (authorization )
716+ homes = HomeData (authorization )
717+ with open ("t.jpg" , "wb" ) as f : f .write (homes .getLiveSnapshot (camera = "LachPresence1" ))
622718 except NoDevice :
623719 if stdout .isatty ():
624720 print ("lnetatmo.py : warning, no home available for testing" )
0 commit comments