Skip to content
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

Simplisafe motion event capture and outdoor camera support #109024

Closed
wants to merge 3,067 commits into from
Closed

Simplisafe motion event capture and outdoor camera support #109024

wants to merge 3,067 commits into from

Conversation

peebles
Copy link
Contributor

@peebles peebles commented Jan 28, 2024

Proposed change

This PR works in conjunction with version 2024.01.0 of simplisafe-python and adds HA support for capturing motion event media files from Simplisafe cameras that support it. In particular, support for the Simplisafe outdoor camera is included.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Originally inspired by bachya/simplisafe-python#85.

A new SimplisafeMotionCamera has been added; a base class that handles motion events from Simplisafe by saving media file urls and providing services for fetching and saving those media files. A new SimplisafeOutdoorCamera has been added that leverages this base class.

Changelog for simplisafe-python library: https://github.com/bachya/simplisafe-python/pull/708/files

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
  • Untested files have been added to .coveragerc.

To help with the load of incoming pull requests:

@home-assistant
Copy link

Hey there @bachya, mind taking a look at this pull request as it has been labeled with an integration (simplisafe) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of simplisafe can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign simplisafe Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component) on the pull request.

@home-assistant home-assistant bot marked this pull request as draft January 29, 2024 18:23
@home-assistant
Copy link

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@bachya
Copy link
Contributor

bachya commented Feb 2, 2024

Looks like you accidentally pulled in other changes. Please rebase this branch off of the latest dev branch.

@peebles
Copy link
Contributor Author

peebles commented Feb 2, 2024

$ git rebase --continue                      
dropping e1795b2eddbd99d86702bdd872b1d88d902454c6 update simplisafe-python version to 2024.01.0 for motion capture support -- patch contents already upstream
dropping 8f23b9ff691c7ab339899e94396c11dbd1170b44 sync with proper EVENT_DISARMED -- patch contents already upstream
dropping 94c45b00504e8d3f478a855048fed5f5d6641a43 add a base class for cameras that support motion, and an OutdoorCamera that uses this base class. -- patch contents already upstream
dropping ea767df7ccd7c32243e52b641915a0ad336c8804 update simplisafe-python version, coverage section -- patch contents already upstream
Successfully rebased and updated refs/heads/simplisafe-outdoor-camera-support.
$ git status
On branch simplisafe-outdoor-camera-support
Your branch and 'origin/simplisafe-outdoor-camera-support' have diverged,
and have 324 and 21 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

nothing to commit, working tree clean

So, should I pull, and then commit? I am pretty sure this is what I did before.

@bachya
Copy link
Contributor

bachya commented Feb 2, 2024

So, should I pull, and then commit? I am pretty sure this is what I did before.

You should force-push this branch after the rebase—that will ensure the unrelated commits are removed.

@peebles
Copy link
Contributor Author

peebles commented Feb 2, 2024

Done:

$ git push origin simplisafe-outdoor-camera-support --force
Enumerating objects: 52, done.
Counting objects: 100% (52/52), done.
Delta compression using up to 12 threads
Compressing objects: 100% (43/43), done.
Writing objects: 100% (43/43), 6.69 KiB | 6.69 MiB/s, done.
Total 43 (delta 36), reused 0 (delta 0)
remote: Resolving deltas: 100% (36/36), completed with 9 local objects.
To github.com:peebles/core.git
 + 0398b13bff...469eabc8a2 simplisafe-outdoor-camera-support -> simplisafe-outdoor-camera-support (forced update)

@peebles
Copy link
Contributor Author

peebles commented Feb 3, 2024

I get a CI failure for "docs-missing". How do I resolve this? I have a PR (home-assistant/home-assistant.io#31110) for the documentation that is open. Does that just need to be approved?

@peebles
Copy link
Contributor Author

peebles commented Feb 14, 2024

Do I need to do anything to wrap this up?

@bachya
Copy link
Contributor

bachya commented Feb 14, 2024

Your PR is in draft, which means it's not ready for review—that's why you haven't gotten interaction. If you're wanting a review, remove it from draft status and I can take a look.

@peebles peebles marked this pull request as ready for review February 14, 2024 02:12
@home-assistant home-assistant bot requested a review from bachya February 14, 2024 02:12
async def async_media_request(self, url: str) -> bytes | None:
"""Fetch a media file."""
content = await self._api.async_media(url)
if content is None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? Can we just return content?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because if I just "return content" mypy says: error: Returning Any from function declared to return "bytes | None" [no-any-return]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And in simplisafe-python (_api) the function is declared:
async def async_media(self, url: str) -> bytes | None:

So I don't understand why mypy is freaking out.


for camera in system.cameras.values():
if camera.camera_type == CameraTypes.OUTDOOR_CAMERA:
cameras.append(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified via a comprehension:

cameras = [
    SimpliSafeOutdoorCamera(hass, simplisafe, system, camera)
    for camera in system.cameras.values()
    if camera.camera_type == CameraTypes.OUTDOOR_CAMERA
]

return self._device.name

@callback
def async_unload(self) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this get called?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does HA call this at some point? I believe I found this sort of thing in other components, so I adopted it here too. If HA does not call it, then I'll remove it ... because I certainly don't call it.

self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image."""
if self._attr_cached_image is not None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ever reset? Are we always returning a cached image once it's been set?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It gets set to None in async_update_from_websocket_event() when a new motion event arrives.

return None

self._attr_cached_image = await self._simplisafe.async_media_request(
self._attr_image_url.replace("{&width}", "&width=" + str(width))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems odd. Why not just construct the entire URL when you need it?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what the urls look like from SS. I do not know the desired width (from a user's automation) until now.

schema=SERVICE_OC_IMAGE_SCHEMA,
)

self._hass.services.async_register(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loop through these service registration calls (since they're almost the same).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only DOMAIN is common. The other 3 (of 4) are unique. Seems to me the loop would be complicated.

homeassistant/components/simplisafe/camera.py Outdated Show resolved Hide resolved
return

width = call.data.get(ATTR_WIDTH)
filename: Template = call.data.get(ATTR_FILENAME) # type: ignore[assignment]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we disabling mypy here?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because if I don't I get: error: Incompatible types in assignment (expression has type "Any | None", variable has type "Template") [assignment]

if self._attr_clip_url is None:
return

filename: Template = call.data.get(ATTR_FILENAME) # type: ignore[assignment]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we disabling mypy here?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I get: error: Incompatible types in assignment (expression has type "Any | None", variable has type "Template") [assignment]

def _register_services(self) -> None:
"""Register the Outdoor Camera hass service calls."""

def _write_image(to_file: str, image_data: bytes) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these nested functions are challenging to follow. What about just including them in the entity?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because they need the reference to self, and wouldn't get it when called from HA as services? I copied this pattern from other components, thinking this is the way to do it.

@nprez83
Copy link
Contributor

nprez83 commented Feb 19, 2024

I've been following this PR and the associated conversations in the other thread closely, so thank you in advance @peebles and @bachya for working on adding this functionality.

I'm noticing the code still refers to outdoor cameras, will this not work with indoor ones? I have one and would be happy to test the code at some point if helpful.

@peebles
Copy link
Contributor Author

peebles commented Mar 3, 2024

I am getting this error when trying to commit:

usage: pylint [options]
pylint: error: Unrecognized option found: ignore-missing-annotations=y

and I see

/workspaces/core/pylint/plugins/hass_enforce_type_hints.py:# be checked by pylint when --ignore-missing-annotations is False
/workspaces/core/pylint/plugins/hass_enforce_type_hints.py: "ignore-missing-annotations",

autinerd and others added 5 commits March 16, 2024 00:03
* Add more sensors

* Fix coverage

* Dont do this rename yet

* Fix case

* Update snapshot

* Add icons

* Remove unused icons

* Update snapshot

* Remove last_value logic from TimeSensor

* Apply suggestions from code review

Co-authored-by: G Johansson <[email protected]>

* Update constant case

* Remove useless test

* Add refresh test back

* Add assertion to post coordinator refresh

---------

Co-authored-by: G Johansson <[email protected]>
@MartinHjelmare
Copy link
Member

Please open a new PR with a fresh branch. This branch has unrelated commits.

@github-actions github-actions bot locked and limited conversation to collaborators Mar 23, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.