@@ -63,13 +63,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
63
63
64
64
out = str (hass .config .path (OUTFILE ))
65
65
66
- sensor = Authenticated (hass , notify , out , exclude , config [CONF_PROVIDER ])
66
+ sensor = AuthenticatedSensor (hass , notify , out , exclude , config [CONF_PROVIDER ])
67
67
sensor .initial_run ()
68
68
69
69
add_devices ([sensor ], True )
70
70
71
71
72
- class Authenticated (Entity ):
72
+ class AuthenticatedBaseException (Exception ):
73
+ """Base exception for Authenticated."""
74
+
75
+ class AuthenticatedRateLimitException (AuthenticatedBaseException ):
76
+ """Ratelimit exception."""
77
+
78
+
79
+ class AuthenticatedSensor (Entity ):
73
80
"""Representation of a Sensor."""
74
81
75
82
def __init__ (self , hass , notify , out , exclude , provider ):
@@ -93,7 +100,6 @@ def initial_run(self):
93
100
_LOGGER .debug ('File has not been created, no data pressent.' )
94
101
95
102
for access in tokens :
96
- accessdata = tokens [access ]
97
103
if access in self .exclude :
98
104
continue
99
105
@@ -102,35 +108,40 @@ def initial_run(self):
102
108
except ValueError :
103
109
continue
104
110
105
- if access in self .stored :
106
- store = self .stored [access ]
107
- access_data = {}
108
- access_data ["last_used_ip" ] = access
109
- access_data ["user_id" ] = store .get ("user_id" )
110
-
111
- if store .get ("last_used_at" ) is not None :
112
- access_data ["last_used_at" ] = store ["last_used_at" ]
113
- elif store .get ("last_authenticated" ) is not None :
114
- access_data ["last_used_at" ] = store ["last_authenticated" ]
115
- else :
116
- access_data ["last_used_at" ] = None
117
-
118
- if store .get ("prev_used_at" ) is not None :
119
- access_data ["prev_used_at" ] = store ["prev_used_at" ]
120
- elif store .get ("previous_authenticated_time" ) is not None :
121
- access_data ["prev_used_at" ] = store ["previous_authenticated_time" ]
122
- else :
123
- access_data ["prev_used_at" ] = None
111
+ accessdata = AuthenticatedData (access , tokens [access ])
124
112
125
- else :
126
- access_data = {
127
- "last_used_ip" : access ,
128
- "user_id" : accessdata ["user_id" ],
129
- "last_used_at" : accessdata ["last_used_at" ],
130
- "prev_used_at" : None
131
- }
132
- ipaddress = IPAddress (access_data , users , self .provider , False )
133
- ipaddress .lookup ()
113
+ if accessdata .ipaddr in self .stored :
114
+ store = AuthenticatedData (accessdata .ipaddr , self .stored [access ])
115
+ accessdata .ipaddr = access
116
+
117
+ if store .user_id is not None :
118
+ accessdata .user_id = store .user_id
119
+
120
+ if store .hostname is not None :
121
+ accessdata .hostname = store .hostname
122
+
123
+ if store .country is not None :
124
+ accessdata .country = store .country
125
+
126
+ if store .region is not None :
127
+ accessdata .region = store .region
128
+
129
+ if store .city is not None :
130
+ accessdata .city = store .city
131
+
132
+ if store .last_access is not None :
133
+ accessdata .last_access = store .last_access
134
+ elif store .attributes .get ("last_authenticated" ) is not None :
135
+ accessdata .last_access = store .attributes ["last_authenticated" ]
136
+
137
+ if store .prev_access is not None :
138
+ accessdata .prev_access = store .prev_access
139
+ elif store .attributes .get ("previous_authenticated_time" ) is not None :
140
+ accessdata .prev_access = store .attributes ["last_authenticated" ]
141
+
142
+ ipaddress = IPData (accessdata , users , self .provider , False )
143
+ if accessdata .ipaddr not in self .stored :
144
+ ipaddress .lookup ()
134
145
self .hass .data [PLATFORM_NAME ][access ] = ipaddress
135
146
self .write_to_file ()
136
147
@@ -139,7 +150,6 @@ def update(self):
139
150
updated = False
140
151
users , tokens = load_authentications (self .hass .config .path (".storage/auth" ))
141
152
for access in tokens :
142
- accessdata = tokens [access ]
143
153
try :
144
154
ValidateIP (access )
145
155
except ValueError :
@@ -163,22 +173,20 @@ def update(self):
163
173
else :
164
174
updated = True
165
175
_LOGGER .warning ("New successfull login from unknown IP (%s)" , access )
166
- access_data = {
167
- "last_used_ip" : access ,
168
- "user_id" : accessdata ["user_id" ],
169
- "last_used_at" : accessdata ["last_used_at" ],
170
- "prev_used_at" : None
171
- }
172
- ipaddress = IPAddress (access_data , users , self .provider )
176
+ accessdata = AuthenticatedData (access , tokens [access ])
177
+ ipaddress = IPData (accessdata , users , self .provider )
173
178
ipaddress .lookup ()
174
179
if ipaddress .new_ip :
175
180
if self .notify :
176
181
ipaddress .notify (self .hass )
177
182
ipaddress .new_ip = False
178
183
184
+ if ipaddress .hostname is None :
185
+ ipaddress .hostname = get_hostname (ipaddress .ip_address )
186
+
179
187
self .hass .data [PLATFORM_NAME ][access ] = ipaddress
180
188
181
- for ipaddr in sorted (tokens ,key = lambda x :tokens [x ]['last_used_at' ], reverse = True ):
189
+ for ipaddr in sorted (tokens , key = lambda x :tokens [x ]['last_used_at' ], reverse = True ):
182
190
self .last_ip = self .hass .data [PLATFORM_NAME ][ipaddr ]
183
191
break
184
192
self ._state = self .last_ip .ip_address
@@ -230,6 +238,7 @@ def write_to_file(self):
230
238
"last_used_at" : known .last_used_at ,
231
239
"prev_used_at" : known .prev_used_at ,
232
240
"country" : known .country ,
241
+ "hostname" : known .hostname ,
233
242
"region" : known .region ,
234
243
"city" : known .city
235
244
}
@@ -308,20 +317,35 @@ def load_authentications(authfile):
308
317
return users , tokens_cleaned
309
318
310
319
311
- class IPAddress :
320
+ class AuthenticatedData :
321
+ """Data class for autenticated values."""
322
+
323
+ def __init__ (self , ipaddr , attributes ):
324
+ """Initialize."""
325
+ self .ipaddr = ipaddr
326
+ self .attributes = attributes
327
+ self .last_access = attributes .get ("last_used_at" )
328
+ self .prev_access = attributes .get ("prev_used_at" )
329
+ self .country = attributes .get ("country" )
330
+ self .region = attributes .get ("region" )
331
+ self .city = attributes .get ("city" )
332
+ self .user_id = attributes .get ("user_id" )
333
+ self .hostname = attributes .get ("hostname" )
334
+
335
+
336
+ class IPData :
312
337
"""IP Address class."""
313
338
def __init__ (self , access_data , users , provider , new = True ):
314
339
self .all_users = users
315
- self .access_data = access_data
316
340
self .provider = provider
317
- self .ip_address = access_data .get ( "last_used_ip" )
318
- self .last_used_at = access_data .get ( "last_used_at" )
319
- self .prev_used_at = access_data .get ( "prev_used_at" )
320
- self .user_id = access_data .get ( " user_id" )
321
- self .hostname = None
322
- self .city = None
323
- self .region = None
324
- self .country = None
341
+ self .ip_address = access_data .ipaddr
342
+ self .last_used_at = access_data .last_access
343
+ self .prev_used_at = access_data .prev_access
344
+ self .user_id = access_data .user_id
345
+ self .hostname = access_data . hostname
346
+ self .city = access_data . city
347
+ self .region = access_data . region
348
+ self .country = access_data . country
325
349
self .new_ip = new
326
350
327
351
@property
@@ -330,12 +354,11 @@ def username(self):
330
354
if self .user_id is None :
331
355
return "Unknown"
332
356
elif self .user_id in self .all_users :
333
- return self .all_users [self .access_data [ " user_id" ] ]
357
+ return self .all_users [self .user_id ]
334
358
return "Unknown"
335
359
336
360
def lookup (self ):
337
361
"""Look up data for the IP address."""
338
- self .hostname = get_hostname (self .ip_address )
339
362
geo = get_geo_data (self .ip_address , self .provider )
340
363
if geo ["result" ]:
341
364
self .country = geo .get ("data" , {}).get ("country" )
@@ -363,11 +386,12 @@ def notify(self, hass):
363
386
last_used_at = ""
364
387
message = """
365
388
**IP Address:** {}
389
+ **Username:** {}
366
390
{}
367
391
{}
368
392
{}
369
393
{}
370
- """ .format (self .ip_address , country , region , city , last_used_at )
394
+ """ .format (self .ip_address , self . username , country , region , city , last_used_at . replace ( "T" , " " ) )
371
395
notify (message , title = 'New successful login' , notification_id = self .ip_address )
372
396
373
397
@@ -385,29 +409,23 @@ def __init__(self, ipaddr):
385
409
def country (self ):
386
410
"""Return country name or None."""
387
411
if self .result :
388
- if "country_name" in self .result :
389
- return self .result ["country_name" ]
390
- elif "country" in self .result :
412
+ if self .result .get ("country" ) is not None :
391
413
return self .result ["country" ]
392
414
return None
393
415
394
416
@property
395
417
def region (self ):
396
418
"""Return region name or None."""
397
419
if self .result :
398
- if "subdivision_1_name" in self .result :
399
- return self .result ["subdivision_1_name" ]
400
- elif "region" in self .result :
420
+ if self .result .get ("region" ) is not None :
401
421
return self .result ["region" ]
402
422
return None
403
423
404
424
@property
405
425
def city (self ):
406
426
"""Return city name or None."""
407
427
if self .result :
408
- if "city_name" in self .result :
409
- return self .result ["city_name" ]
410
- elif "city" in self .result :
428
+ if self .result .get ("city" ) is not None :
411
429
return self .result ["city" ]
412
430
return None
413
431
@@ -427,26 +445,50 @@ def update_geo_info(self):
427
445
self .result = {}
428
446
try :
429
447
api = self .url .format (self .ipaddr )
430
- data = requests .get (api , timeout = 5 ).json ()
448
+ header = {
449
+ "user-agent" : "Home Assistant/Python"
450
+ }
451
+ data = requests .get (api , headers = header , timeout = 5 ).json ()
431
452
432
- if 'reserved' in str (data ):
433
- return
434
- elif data .get ('status' ) != 'success' :
453
+ if data .get ("error" ):
454
+ if data .get ("reason" ) == "RateLimited" :
455
+ raise AuthenticatedRateLimitException (
456
+ "RatelimitError, try a different provider." )
457
+
458
+ elif data .get ("status" , "success" ) == "error" :
435
459
return
436
- elif 'Private' in data .get ('org' ):
460
+
461
+ elif data .get ("reserved" ):
437
462
return
438
463
439
- if data .get ('data' ) is not None :
440
- data = data ["data" ]
464
+ elif data .get ("status" , "success" ) == "fail" :
465
+ raise AuthenticatedBaseException (
466
+ "[{}] - {}" .format (self .ipaddr , data .get ("message" , "Unkown error." )))
467
+
441
468
self .result = data
442
- except Exception : # pylint: disable=broad-except
443
- return
469
+ self .parse_data ()
470
+ except AuthenticatedRateLimitException as exception :
471
+ _LOGGER .error (exception )
472
+ except AuthenticatedBaseException as exception :
473
+ _LOGGER .error (exception )
474
+ except requests .exceptions .ConnectionError :
475
+ pass
444
476
477
+ def parse_data (self ):
478
+ """Parse data from geoprovider."""
479
+ self .result = self .result
445
480
446
481
class IPApi (GeoProvider ):
447
482
"""IPApi class."""
448
483
url = "https://ipapi.co/{}/json"
449
484
485
+ @property
486
+ def country (self ):
487
+ """Return country name or None."""
488
+ if self .result :
489
+ if self .result .get ("country_name" ) is not None :
490
+ return self .result ["country_name" ]
491
+ return None
450
492
451
493
class ExtremeIPLookup (GeoProvider ):
452
494
"""IPApi class."""
@@ -456,3 +498,31 @@ class ExtremeIPLookup(GeoProvider):
456
498
class IPVigilante (GeoProvider ):
457
499
"""IPVigilante class."""
458
500
url = "https://ipvigilante.com/json/{}"
501
+
502
+ def parse_data (self ):
503
+ """Parse data from geoprovider."""
504
+ self .result = self .result .get ("data" , {})
505
+
506
+ @property
507
+ def country (self ):
508
+ """Return country name or None."""
509
+ if self .result :
510
+ if self .result .get ("country_name" ) is not None :
511
+ return self .result ["country_name" ]
512
+ return None
513
+
514
+ @property
515
+ def region (self ):
516
+ """Return region name or None."""
517
+ if self .result :
518
+ if self .result .get ("subdivision_1_name" ) is not None :
519
+ return self .result ["subdivision_1_name" ]
520
+ return None
521
+
522
+ @property
523
+ def city (self ):
524
+ """Return city name or None."""
525
+ if self .result :
526
+ if self .result .get ("city_name" ) is not None :
527
+ return self .result ["city_name" ]
528
+ return None
0 commit comments