44from enum import Enum
55from datetime import datetime , timedelta , timezone
66from typing import Dict
7-
7+ import base64
8+ import io
89import requests
10+ from PIL import Image
911
1012from .util import robustTimeParse , toBool
1113from .addressable import AddressableLeaf , AddressableObject , AddressableAttribute , AddressableDict , AddressableList , \
@@ -31,6 +33,8 @@ def __init__(
3133 parent ,
3234 fromDict ,
3335 fixAPI = True ,
36+ updateCapabilities = True ,
37+ updatePictures = True ,
3438 ):
3539 self .weConnect = weConnect
3640 super ().__init__ (localAddress = vin , parent = parent )
@@ -47,12 +51,16 @@ def __init__(
4751 self .controls = Controls (localAddress = 'controls' , vehicle = self , parent = self )
4852 self .fixAPI = fixAPI
4953
50- self .update (fromDict )
54+ self .__carImages = dict ()
55+ self .pictures = AddressableDict (localAddress = 'pictures' , parent = self )
56+
57+ self .update (fromDict , updateCapabilities = updateCapabilities , updatePictures = updatePictures )
5158
5259 def update ( # noqa: C901 # pylint: disable=too-many-branches
5360 self ,
5461 fromDict = None ,
5562 updateCapabilities = True ,
63+ updatePictures = True ,
5664 force = False ,
5765 ):
5866 if fromDict is not None :
@@ -145,7 +153,8 @@ def update( # noqa: C901 # pylint: disable=too-many-branches
145153 LOG .warning ('%s: Unknown attribute %s with value %s' , self .getGlobalAddress (), key , value )
146154
147155 self .updateStatus (updateCapabilities = updateCapabilities , force = force )
148- # self.test()
156+ if updatePictures :
157+ self .updatePictures ()
149158
150159 def updateStatus (self , updateCapabilities = True , force = False ): # noqa: C901 # pylint: disable=too-many-branches
151160 data = None
@@ -288,10 +297,145 @@ def updateStatus(self, updateCapabilities=True, force=False): # noqa: C901 # py
288297 parent = self .statuses ,
289298 statusId = 'parkingPosition' ,
290299 fromDict = data ['data' ])
291- return
292- if 'parkingPosition' in self .statuses :
300+ elif 'parkingPosition' in self .statuses :
293301 del self .statuses ['parkingPosition' ]
294302
303+ def updatePictures (self ): # noqa: C901
304+ data = None
305+ cacheDate = None
306+ url = f'https://vehicle-images-service.apps.emea.vwapps.io/v2/vehicle-images/{ self .vin .value } ?resolution=2x'
307+ if self .weConnect .maxAge is not None and self .weConnect .cache is not None and url in self .weConnect .cache :
308+ data , cacheDateString = self .weConnect .cache [url ]
309+ cacheDate = datetime .fromisoformat (cacheDateString )
310+ if data is None or self .weConnect .maxAge is None \
311+ or (cacheDate is not None and cacheDate < (datetime .utcnow () - timedelta (seconds = self .weConnect .maxAge ))):
312+ imageResponse = self .weConnect .session .get (url , allow_redirects = False )
313+ if imageResponse .status_code == requests .codes ['ok' ]:
314+ data = imageResponse .json ()
315+ if self .weConnect .cache is not None :
316+ self .weConnect .cache [url ] = (data , str (datetime .utcnow ()))
317+ elif imageResponse .status_code == requests .codes ['unauthorized' ]:
318+ LOG .info ('Server asks for new authorization' )
319+ self .weConnect .login ()
320+ imageResponse = self .weConnect .session .get (url , allow_redirects = False )
321+ if imageResponse .status_code == requests .codes ['ok' ]:
322+ data = imageResponse .json ()
323+ if self .weConnect .cache is not None :
324+ self .weConnect .cache [url ] = (data , str (datetime .utcnow ()))
325+ else :
326+ raise RetrievalError ('Could not retrieve data even after re-authorization.'
327+ f' Status Code was: { imageResponse .status_code } ' )
328+ raise RetrievalError (f'Could not retrieve data. Status Code was: { imageResponse .status_code } ' )
329+ if data is not None and 'data' in data : # pylint: disable=too-many-nested-blocks
330+ for image in data ['data' ]:
331+ img = None
332+ cacheDate = None
333+ url = image ['url' ]
334+ if self .weConnect .maxAge is not None and self .weConnect .cache is not None and url in self .weConnect .cache :
335+ img , cacheDateString = self .weConnect .cache [url ]
336+ img = base64 .b64decode (img )
337+ img = Image .open (io .BytesIO (img ))
338+ cacheDate = datetime .fromisoformat (cacheDateString )
339+ if img is None or self .weConnect .maxAge is None \
340+ or (cacheDate is not None and cacheDate < (datetime .utcnow () - timedelta (days = 1 ))):
341+ imageDownloadResponse = self .weConnect .session .get (url , stream = True )
342+ if imageDownloadResponse .status_code == requests .codes ['ok' ]:
343+ img = Image .open (imageDownloadResponse .raw )
344+ if self .weConnect .cache is not None :
345+ buffered = io .BytesIO ()
346+ img .save (buffered , format = "PNG" )
347+ imgStr = base64 .b64encode (buffered .getvalue ()).decode ("utf-8" )
348+ self .weConnect .cache [url ] = (imgStr , str (datetime .utcnow ()))
349+ elif imageDownloadResponse .status_code == requests .codes ['unauthorized' ]:
350+ LOG .info ('Server asks for new authorization' )
351+ self .weConnect .login ()
352+ imageDownloadResponse = self .weConnect .session .get (url , stream = True )
353+ if imageDownloadResponse .status_code == requests .codes ['ok' ]:
354+ img = Image .open (imageDownloadResponse .raw )
355+ if self .weConnect .cache is not None :
356+ buffered = io .BytesIO ()
357+ img .save (buffered , format = "PNG" )
358+ imgStr = base64 .b64encode (buffered .getvalue ()).decode ("utf-8" )
359+ self .weConnect .cache [url ] = (imgStr , str (datetime .utcnow ()))
360+ else :
361+ raise RetrievalError ('Could not retrieve data even after re-authorization.'
362+ f' Status Code was: { imageDownloadResponse .status_code } ' )
363+ raise RetrievalError (f'Could not retrieve data. Status Code was: { imageDownloadResponse .status_code } ' )
364+
365+ if img is not None :
366+ self .__carImages [image ['id' ]] = img
367+ if image ['id' ] == 'car_34view' :
368+ if 'car' in self .pictures :
369+ self .pictures ['car' ].setValueWithCarTime (self .__carImages ['car_34view' ], lastUpdateFromCar = None , fromServer = True )
370+ else :
371+ self .pictures ['car' ] = AddressableAttribute (localAddress = 'car' , parent = self .pictures , value = self .__carImages ['car_34view' ],
372+ valueType = Image .Image )
373+
374+ self .updateStatusPicture ()
375+
376+ def updateStatusPicture (self ): # noqa: C901
377+ img = self .__carImages ['car_birdview' ]
378+
379+ if 'accessStatus' in self .statuses :
380+ accessStatus = self .statuses ['accessStatus' ]
381+ for name , door in accessStatus .doors .items ():
382+ doorNameMap = {'frontLeft' : 'door_left_front' ,
383+ 'frontRight' : 'door_right_front' ,
384+ 'rearLeft' : 'door_left_back' ,
385+ 'rearRight' : 'door_right_back' }
386+ name = doorNameMap .get (name , name )
387+ doorImageName = None
388+
389+ if door .openState .value in (AccessStatus .Door .OpenState .OPEN , AccessStatus .Door .OpenState .INVALID ):
390+ doorImageName = f'{ name } _overlay'
391+ elif door .openState .value == AccessStatus .Door .OpenState .CLOSED :
392+ doorImageName = name
393+
394+ if doorImageName is not None and doorImageName in self .__carImages :
395+ doorImage = self .__carImages [doorImageName ].convert ("RGBA" )
396+ img .paste (doorImage , (0 , 0 ), doorImage )
397+
398+ for name , window in accessStatus .windows .items ():
399+ windowNameMap = {'frontLeft' : 'window_left_front' ,
400+ 'frontRight' : 'window_right_front' ,
401+ 'rearLeft' : 'window_left_back' ,
402+ 'rearRight' : 'window_right_back' ,
403+ 'sunRoof' : 'sunroof' }
404+ name = windowNameMap .get (name , name )
405+ windowImageName = None
406+
407+ if window .openState .value in (AccessStatus .Window .OpenState .OPEN , AccessStatus .Window .OpenState .INVALID ):
408+ windowImageName = f'{ name } _overlay'
409+ elif window .openState .value == AccessStatus .Window .OpenState .CLOSED :
410+ windowImageName = f'{ name } '
411+
412+ if windowImageName is not None and windowImageName in self .__carImages :
413+ windowImage = self .__carImages [windowImageName ].convert ("RGBA" )
414+ img .paste (windowImage , (0 , 0 ), windowImage )
415+
416+ if 'lightsStatus' in self .statuses :
417+ lightsStatus = self .statuses ['lightsStatus' ]
418+ for name , light in lightsStatus .lights .items ():
419+ lightNameMap = {'frontLeft' : 'door_left_front' ,
420+ 'frontRight' : 'door_right_front' ,
421+ 'rearLeft' : 'door_left_back' ,
422+ 'rearRight' : 'door_right_back' }
423+ name = lightNameMap .get (name , name )
424+ lightImageName = None
425+
426+ if light .status .value == LightsStatus .Light .LightState .ON :
427+ lightImageName = f'light_{ name } '
428+ if lightImageName in self .__carImages :
429+ lightImage = self .__carImages [lightImageName ].convert ("RGBA" )
430+ img .paste (lightImage , (0 , 0 ), lightImage )
431+
432+ self .__carImages ['status' ] = img
433+
434+ if 'status' in self .pictures :
435+ self .pictures ['status' ].setValueWithCarTime (img , lastUpdateFromCar = None , fromServer = True )
436+ else :
437+ self .pictures ['status' ] = AddressableAttribute (localAddress = 'status' , parent = self .pictures , value = img , valueType = Image .Image )
438+
295439 def __str__ (self ): # noqa: C901
296440 returnString = ''
297441 if self .vin .enabled :
0 commit comments