-
-
Notifications
You must be signed in to change notification settings - Fork 37.8k
Add facebox auth #15439
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
Add facebox auth #15439
Changes from 4 commits
e50ce87
61105e4
e6cf59e
13f70cd
53a8846
ffe1c5c
259bb13
a5d981b
c2fa465
788a8e5
ccb7247
a00389b
6aaa88a
e07af2e
08d66c9
42529d7
c0298b6
72f08fd
df5e05d
509a4b5
2cfc529
7006994
6154103
f673150
136e7c8
303ff24
871c272
ad5008c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,32 +10,38 @@ | |
| import requests | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.const import ( | ||
| ATTR_ENTITY_ID, ATTR_NAME) | ||
| from homeassistant.const import ATTR_ENTITY_ID | ||
| from homeassistant.core import split_entity_id | ||
| import homeassistant.helpers.config_validation as cv | ||
| from homeassistant.components.image_processing import ( | ||
| PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_CONFIDENCE, CONF_SOURCE, | ||
| CONF_ENTITY_ID, CONF_NAME, DOMAIN) | ||
| from homeassistant.const import (CONF_IP_ADDRESS, CONF_PORT) | ||
| from homeassistant.const import ( | ||
| CONF_IP_ADDRESS, CONF_PORT, CONF_PASSWORD, CONF_USERNAME, | ||
| HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED) | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| ATTR_BOUNDING_BOX = 'bounding_box' | ||
| ATTR_CLASSIFIER = 'classifier' | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Really I see this in |
||
| ATTR_IMAGE_ID = 'image_id' | ||
| ATTR_ID = 'id' | ||
| ATTR_MATCHED = 'matched' | ||
| ATTR_NAME = 'name' | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're using Please import
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The variables are getting really confusing, happy to just use
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My point is that we shouldn't use the same constant when interfacing with two separate APIs. One API might change and then we might change the constant to go along with the changed API, and then we would break the usage of the other API.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally agree |
||
| CLASSIFIER = 'facebox' | ||
| DATA_FACEBOX = 'facebox_classifiers' | ||
| EVENT_CLASSIFIER_TEACH = 'image_processing.teach_classifier' | ||
| FILE_PATH = 'file_path' | ||
| NOTIFICATION_ID = 'facebox_notification' | ||
| NOTIFICATION_TITLE = 'facebox teach' | ||
| SERVICE_TEACH_FACE = 'facebox_teach_face' | ||
| TIMEOUT = 9 | ||
|
|
||
|
|
||
| PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ | ||
| vol.Required(CONF_IP_ADDRESS): cv.string, | ||
| vol.Required(CONF_PORT): cv.port, | ||
| vol.Optional(CONF_USERNAME): cv.string, | ||
| vol.Optional(CONF_PASSWORD): cv.string, | ||
| }) | ||
|
|
||
| SERVICE_TEACH_SCHEMA = vol.Schema({ | ||
|
|
@@ -75,15 +81,44 @@ def parse_faces(api_faces): | |
| return known_faces | ||
|
|
||
|
|
||
| def post_image(url, image): | ||
| def post_image(url, username, password, image): | ||
| """Post an image to the classifier.""" | ||
| try: | ||
| response = requests.post( | ||
| url, | ||
| auth=requests.auth.HTTPBasicAuth(username, password), | ||
| json={"base64": encode_image(image)}, | ||
| timeout=TIMEOUT | ||
| ) | ||
| return response | ||
| if response.status_code == HTTP_UNAUTHORIZED: | ||
| _LOGGER.error("AuthenticationError on %s", CLASSIFIER) | ||
| return None | ||
| else: | ||
| return response | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we return something here, the other places that this function can exit at are lacking a return statement. If a function returns something, all exits need to return something. Consistency is good.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK |
||
| except requests.exceptions.ConnectionError: | ||
| _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER) | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
|
|
||
| def teach_file(url, username, password, name, file_path): | ||
| """Teach the classifier a name associated with a file.""" | ||
| try: | ||
| with open(file_path, 'rb') as open_file: | ||
| response = requests.post( | ||
| url, | ||
| auth=requests.auth.HTTPBasicAuth(username, password), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if username or password is None?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They are by default and its OK
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't send in auth if username and password are kwargs = {}
if username:
kwargs['auth'] = requests.auth.HTTPBasicAuth(username, password)
response = request.post(url, **kwargs) |
||
| data={ATTR_NAME: name, ATTR_ID: file_path}, | ||
| files={'file': open_file}, | ||
| timeout=TIMEOUT | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Be careful with the default timeout as this is sending files
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will remove |
||
| ) | ||
| if response.status_code == HTTP_UNAUTHORIZED: | ||
| _LOGGER.error("AuthenticationError on %s", CLASSIFIER) | ||
| return None | ||
| elif response.status_code == HTTP_BAD_REQUEST: | ||
| _LOGGER.error("%s teaching of file %s failed with message:%s", | ||
| CLASSIFIER, file_path, response.text) | ||
| return None | ||
| else: | ||
| return response | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See about consistent return.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK |
||
| except requests.exceptions.ConnectionError: | ||
| _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER) | ||
|
|
||
|
|
@@ -104,12 +139,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): | |
| if DATA_FACEBOX not in hass.data: | ||
| hass.data[DATA_FACEBOX] = [] | ||
|
|
||
| ip = config[CONF_IP_ADDRESS] | ||
| port = config[CONF_PORT] | ||
| username = config.get(CONF_USERNAME) | ||
| password = config.get(CONF_PASSWORD) | ||
|
|
||
| entities = [] | ||
| for camera in config[CONF_SOURCE]: | ||
| facebox = FaceClassifyEntity( | ||
| config[CONF_IP_ADDRESS], | ||
| config[CONF_PORT], | ||
| camera[CONF_ENTITY_ID], | ||
| ip, port, username, password, camera[CONF_ENTITY_ID], | ||
| camera.get(CONF_NAME)) | ||
| entities.append(facebox) | ||
| hass.data[DATA_FACEBOX].append(facebox) | ||
|
|
@@ -129,33 +167,33 @@ def service_handle(service): | |
| classifier.teach(name, file_path) | ||
|
|
||
| hass.services.register( | ||
| DOMAIN, | ||
| SERVICE_TEACH_FACE, | ||
| service_handle, | ||
| DOMAIN, SERVICE_TEACH_FACE, service_handle, | ||
| schema=SERVICE_TEACH_SCHEMA) | ||
|
|
||
|
|
||
| class FaceClassifyEntity(ImageProcessingFaceEntity): | ||
| """Perform a face classification.""" | ||
|
|
||
| def __init__(self, ip, port, camera_entity, name=None): | ||
| def __init__(self, ip, port, username, password, camera_entity, name=None): | ||
| """Init with the API key and model id.""" | ||
| super().__init__() | ||
| self._url_check = "http://{}:{}/{}/check".format(ip, port, CLASSIFIER) | ||
| self._url_teach = "http://{}:{}/{}/teach".format(ip, port, CLASSIFIER) | ||
| self._username = username | ||
| self._password = password | ||
| self._camera = camera_entity | ||
| if name: | ||
| self._name = name | ||
| else: | ||
| camera_name = split_entity_id(camera_entity)[1] | ||
| self._name = "{} {}".format( | ||
| CLASSIFIER, camera_name) | ||
| self._name = "{} {}".format(CLASSIFIER, camera_name) | ||
| self._matched = {} | ||
|
|
||
| def process_image(self, image): | ||
| """Process an image.""" | ||
| response = post_image(self._url_check, image) | ||
| if response is not None: | ||
| response = post_image( | ||
| self._url_check, self._username, self._password, image) | ||
| if response: | ||
| response_json = response.json() | ||
| if response_json['success']: | ||
| total_faces = response_json['facesCount'] | ||
|
|
@@ -173,34 +211,8 @@ def teach(self, name, file_path): | |
| if (not self.hass.config.is_allowed_path(file_path) | ||
| or not valid_file_path(file_path)): | ||
| return | ||
| with open(file_path, 'rb') as open_file: | ||
| response = requests.post( | ||
| self._url_teach, | ||
| data={ATTR_NAME: name, 'id': file_path}, | ||
| files={'file': open_file}) | ||
|
|
||
| if response.status_code == 200: | ||
| self.hass.bus.fire( | ||
| EVENT_CLASSIFIER_TEACH, { | ||
| ATTR_CLASSIFIER: CLASSIFIER, | ||
| ATTR_NAME: name, | ||
| FILE_PATH: file_path, | ||
| 'success': True, | ||
| 'message': None | ||
| }) | ||
|
|
||
| elif response.status_code == 400: | ||
| _LOGGER.warning( | ||
| "%s teaching of file %s failed with message:%s", | ||
| CLASSIFIER, file_path, response.text) | ||
| self.hass.bus.fire( | ||
| EVENT_CLASSIFIER_TEACH, { | ||
| ATTR_CLASSIFIER: CLASSIFIER, | ||
| ATTR_NAME: name, | ||
| FILE_PATH: file_path, | ||
| 'success': False, | ||
| 'message': response.text | ||
| }) | ||
| teach_file( | ||
| self._url_teach, self._username, self._password, name, file_path) | ||
|
|
||
| @property | ||
| def camera_entity(self): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see this as a platform variable