-
-
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 1 commit
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 |
|---|---|---|
|
|
@@ -17,7 +17,9 @@ | |
| 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_OK, HTTP_UNAUTHORIZED) | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -36,6 +38,8 @@ | |
| 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 +79,36 @@ 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) | ||
| else: | ||
| return response | ||
| 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, 'id': file_path}, | ||
|
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. As this api is not part of home assistant, we shouldn't use constants that are used internally and across home assistant, and might change, ie
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. It's ok to define and use local constants to be used with the facebox api in this module though. But then do so consistently.
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. My plan is to refactor these back into the |
||
| files={'file': open_file}) | ||
| if response.status_code == HTTP_UNAUTHORIZED: | ||
| _LOGGER.error("AuthenticationError on %s", CLASSIFIER) | ||
| 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,11 +129,22 @@ 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 = None | ||
| if CONF_USERNAME in config: | ||
| username = config[CONF_USERNAME] | ||
|
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. username = config.get(CONF_USERNAME)This will default username to
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. Good to know..! |
||
| password = None | ||
| if CONF_PASSWORD in config: | ||
| password = config[CONF_PASSWORD] | ||
|
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 above.
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 |
||
|
|
||
| entities = [] | ||
| for camera in config[CONF_SOURCE]: | ||
| facebox = FaceClassifyEntity( | ||
| config[CONF_IP_ADDRESS], | ||
| config[CONF_PORT], | ||
| ip, | ||
|
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. Please continue the line as far as possible before breaking the line. But keep the first break after the first parenthesis. This is more in line (😉) with home assistant style.
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 |
||
| port, | ||
| username, | ||
| password, | ||
| camera[CONF_ENTITY_ID], | ||
| camera.get(CONF_NAME)) | ||
| entities.append(facebox) | ||
|
|
@@ -138,11 +174,13 @@ def service_handle(service): | |
| 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 | ||
|
|
@@ -154,7 +192,10 @@ def __init__(self, ip, port, camera_entity, name=None): | |
|
|
||
| def process_image(self, image): | ||
| """Process an image.""" | ||
| response = post_image(self._url_check, image) | ||
| response = post_image(self._url_check, | ||
|
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 above about line break style.
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 |
||
| self._username, | ||
| self._password, | ||
| image) | ||
| if response is not None: | ||
| response_json = response.json() | ||
| if response_json['success']: | ||
|
|
@@ -173,34 +214,36 @@ 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 | ||
| }) | ||
| response = teach_file(self._url_teach, | ||
|
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 above about line break style.
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 |
||
| self._username, | ||
| self._password, | ||
| name, | ||
| file_path) | ||
|
|
||
| if response is not None: | ||
|
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. Use a guard clause here, by inverting this check and returning if it's true.
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 |
||
| if response.status_code == HTTP_OK: | ||
| 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 == HTTP_BAD_REQUEST: | ||
| _LOGGER.warning( | ||
| "%s teaching of file %s failed with message:%s", | ||
| CLASSIFIER, file_path, response.text) | ||
| self.hass.bus.fire( | ||
|
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. What is the benefit of firing an event also for failure?
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. I have an automation to send a notification on teaching, but perhaps I just drop both events (success & fail) here, and just use the
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. You know better how the events are useful than me. What is the workflow for teaching you are using or imagining for the future? If we lay that down, it'll probably be more clear what is useful.
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. One of the issues with the facebox free tier is that if 'forgets' faces if you restart the docker container, so I imagine using the service for allowing automations to reteach a folder of faces. I now think that a log message is fine if teaching fails for some reason
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. persistent notification will be better then a log entry :)
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. Have you got an example of notification?
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. hass.components.persistent_notification.create( |
||
| EVENT_CLASSIFIER_TEACH, { | ||
| ATTR_CLASSIFIER: CLASSIFIER, | ||
| ATTR_NAME: name, | ||
| FILE_PATH: file_path, | ||
| 'success': False, | ||
| 'message': response.text | ||
| }) | ||
|
|
||
| @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.
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.
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.
OK