6
6
https://home-assistant.io/components/camera/
7
7
"""
8
8
import asyncio
9
+ import collections
9
10
from datetime import timedelta
10
11
import logging
11
12
import hashlib
13
+ from random import SystemRandom
12
14
13
15
import aiohttp
14
16
from aiohttp import web
15
17
import async_timeout
16
18
19
+ from homeassistant .core import callback
17
20
from homeassistant .const import ATTR_ENTITY_PICTURE
18
21
from homeassistant .exceptions import HomeAssistantError
19
22
from homeassistant .helpers .aiohttp_client import async_get_clientsession
20
23
from homeassistant .helpers .entity import Entity
21
24
from homeassistant .helpers .entity_component import EntityComponent
22
25
from homeassistant .helpers .config_validation import PLATFORM_SCHEMA # noqa
23
26
from homeassistant .components .http import HomeAssistantView , KEY_AUTHENTICATED
27
+ from homeassistant .helpers .event import async_track_time_interval
24
28
25
29
_LOGGER = logging .getLogger (__name__ )
26
30
35
39
36
40
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
37
41
42
+ TOKEN_CHANGE_INTERVAL = timedelta (minutes = 5 )
43
+ _RND = SystemRandom ()
44
+
38
45
39
46
@asyncio .coroutine
40
47
def async_get_image (hass , entity_id , timeout = 10 ):
@@ -80,6 +87,15 @@ def async_setup(hass, config):
80
87
hass .http .register_view (CameraMjpegStream (component .entities ))
81
88
82
89
yield from component .async_setup (config )
90
+
91
+ @callback
92
+ def update_tokens (time ):
93
+ """Update tokens of the entities."""
94
+ for entity in component .entities .values ():
95
+ entity .async_update_token ()
96
+ hass .async_add_job (entity .async_update_ha_state ())
97
+
98
+ async_track_time_interval (hass , update_tokens , TOKEN_CHANGE_INTERVAL )
83
99
return True
84
100
85
101
@@ -89,13 +105,8 @@ class Camera(Entity):
89
105
def __init__ (self ):
90
106
"""Initialize a camera."""
91
107
self .is_streaming = False
92
- self ._access_token = hashlib .sha256 (
93
- str .encode (str (id (self )))).hexdigest ()
94
-
95
- @property
96
- def access_token (self ):
97
- """Access token for this camera."""
98
- return self ._access_token
108
+ self .access_tokens = collections .deque ([], 2 )
109
+ self .async_update_token ()
99
110
100
111
@property
101
112
def should_poll (self ):
@@ -105,7 +116,7 @@ def should_poll(self):
105
116
@property
106
117
def entity_picture (self ):
107
118
"""Return a link to the camera feed as entity picture."""
108
- return ENTITY_IMAGE_URL .format (self .entity_id , self .access_token )
119
+ return ENTITY_IMAGE_URL .format (self .entity_id , self .access_tokens [ - 1 ] )
109
120
110
121
@property
111
122
def is_recording (self ):
@@ -196,7 +207,7 @@ def state(self):
196
207
def state_attributes (self ):
197
208
"""Camera state attributes."""
198
209
attr = {
199
- 'access_token' : self .access_token ,
210
+ 'access_token' : self .access_tokens [ - 1 ] ,
200
211
}
201
212
202
213
if self .model :
@@ -207,6 +218,13 @@ def state_attributes(self):
207
218
208
219
return attr
209
220
221
+ @callback
222
+ def async_update_token (self ):
223
+ """Update the used token."""
224
+ self .access_tokens .append (
225
+ hashlib .sha256 (
226
+ _RND .getrandbits (256 ).to_bytes (32 , 'little' )).hexdigest ())
227
+
210
228
211
229
class CameraView (HomeAssistantView ):
212
230
"""Base CameraView."""
@@ -223,10 +241,11 @@ def get(self, request, entity_id):
223
241
camera = self .entities .get (entity_id )
224
242
225
243
if camera is None :
226
- return web .Response (status = 404 )
244
+ status = 404 if request [KEY_AUTHENTICATED ] else 401
245
+ return web .Response (status = status )
227
246
228
247
authenticated = (request [KEY_AUTHENTICATED ] or
229
- request .GET .get ('token' ) == camera .access_token )
248
+ request .GET .get ('token' ) in camera .access_tokens )
230
249
231
250
if not authenticated :
232
251
return web .Response (status = 401 )
0 commit comments