bachya / simplisafe-python

🚨 A Python3, asyncio-based interface to the SimpliSafe™ API
https://simplisafe-python.readthedocs.io
MIT License
50 stars 31 forks source link

SimpliSafe Camera Integration #85

Open tbrock47 opened 4 years ago

tbrock47 commented 4 years ago

So the "SimpliCam" as they call it is a combination camera and motion sensor. The camera itself is joined to your local network wifi during setup. I am unsure if the included sensor is also communicating via the local network or their standard communication protocol.

You stated you don't have a camera of your own, so let me know how I can help.

bachya commented 4 years ago

Thanks for offering to help! I need to understand how the SimpliSafe web app communicates with the camera. Here's my suggestion:

  1. Open Google Chrome.
  2. Navigate to https://webapp.simplisafe.com and login.
  3. Open the Chrome Developer Tools (View >> Developer >> Developer Tools).
  4. Browse around various camera "operations" – open the camera page, view the camera, stop viewing the camera, go elsewhere, etc.
  5. When done, go back to the Developer Tools and click the Export HAR link (see below). Don't post that here; send it to me in a DM on the HASS Community Forum.

Screen Shot 2020-01-28 at 11 33 14 AM

bachya commented 4 years ago

Thanks for the payload, @tbrock47. Here's what I've learned:

Once you log into the web app and access the camera page, a URL like this is request:

https://media.simplisafe.com/v1/<SOME_ID>/flv?x=472&audioEncoding=AAC

I assume that this is the stream URL to access, although when I attempted to access yours, I got Unauthorized. I'm assuming this is either because (a) the access token is shorted lived or (b) I got the headers wrong.

I'll continue to investigate.

KTibow commented 4 years ago

Also, how does the operation of viewing previous motion events work?

tbrock47 commented 4 years ago

Also, how does the operation of viewing previous motion events work?

The motion events aren't logged on the Cameras page. They show up as a line item under the Timeline page as "Camera Motion Detected". The Cameras page is just a tile layout of all your cameras which allows you to click on one to get a live view.

I'm honestly more interested in just getting a live stream over anything else. The reset would be a bonus for a later date IMO.

KTibow commented 4 years ago

Got it. But once you get the livestream going, when you add it to Home Assistant, you probably won’t want to have live view always on. (Maybe take a “snapshot” every hour.)

ph1ash commented 4 years ago

Hey all - been digging into this as well. So the <SOME_ID> (which is a unique camera ID) mentioned above comes through during the https://api.simplisafe.com/v1/users/<user_id>/subscriptions GET request. In the JSON response, you'll find your camera ID in subscriptions-><first subscription entry>->location->system->cameras-><first entry>->uuid . This is then used to point to the link mentioned above; https://media.simplisafe.com/v1/<Camera UUID>/flv?x=472&audioEncoding=AAC. I'm having trouble with the get_systems call at the moment, but will try to resolve that soon and then try fetching the uuid through the get_systems API (might already be coming through but I can't tell unfortunately).

bachya commented 4 years ago

Thanks @ph1ash! Let me know what I can to to assist you.

archerne commented 4 years ago

Out of curiosity, the SimpliSafe camera is available to view on the SimpliSafe app without any subscription, are you looking into a way to be able to view the camera without pulling the subscription settings?

jaredswarts55 commented 4 years ago

I don't really know what I am doing in python but I was able to stream the flv to a file and watch it with vlc using this code:


import asyncio

from aiohttp import ClientSession
from simplipy import *

async def get_subscriptions(user_id: str, access_token: str, client: ClientSession):
    url = 'https://api.simplisafe.com/v1/users/{}/subscriptions'.format(user_id)
    print("Url To Send Call To: {}".format(url))
    async with client.get(url, headers={'Authorization': 'Bearer {}'.format(access_token)}) as resp:
        return await resp.json()

async def get_camera_feed(camera_id: str, access_token: str, client: ClientSession):
    url = 'https://media.simplisafe.com/v1/{}/flv?x=472&audioEncoding=AAC'.format(camera_id)
    print("Url To Send Call To: {}".format(url))
    with open('testing.flv', 'wb') as file:
        async with client.get(url, headers={'Authorization': 'Bearer {}'.format(access_token)}) as resp:
            async for data in resp.content.iter_chunked(1024):
                file.write(data)

async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as session:
        simplisafe = await API.login_via_credentials(
            "YOUR_EMAIL_HERE", "YOUR_PASSWORD_HERE", session=session
        )

        systems = await simplisafe.get_systems();
        for s in systems:
            system = systems[s]

        subscriptionsBody = await get_subscriptions(simplisafe.user_id, simplisafe.access_token, session)
        cameraId = subscriptionsBody["subscriptions"][0]["location"]["system"]["cameras"][0]["uuid"]
        print("Camera Id: {}".format(cameraId))
        print("Subscriptions: {}".format(subscriptionsBody))
        await get_camera_feed(cameraId, simplisafe.access_token, session)

asyncio.run(main())
KTibow commented 4 years ago

@jaredswarts55 what do you mean "stream"? Did you just press Ctrl+C to stop it when you were done? I'd be interested if there was a way to get past camera recordings too.

jaredswarts55 commented 4 years ago

@KTibow yeah, the request was open and streaming to the file, increasing its file size, I had to hit Ctrl+C to stop it.

KTibow commented 4 years ago

@jaredswarts55 so at the end of it did VLC complain about invalidness, or was it okay since you chunked them into 1024-byte parts?

shamoon commented 4 years ago

Hey guys, I work on a node implementation of the SS api for Apple HomeKit nzapponi/homebridge-simplisafe3, we've had camera integration for a while though obviously its different we pipe through ffmpeg to an rstp stream (which is what Apple requires).

Unfortunately it seems theres no way to locally tap into the cameras as you've noted but you guys have the right URL FWIW, theres also an mjpeg stream available at: https://media.simplisafe.com/v1/{cameraID}/mjpg?x=${width}&fr=1, and finally wanted to mention if you leave off the audioEncoding=AAC you will get the h.264 stream with speex audio instead of aac.

Good luck! I will be following along in case I can learn something for my project =)

jaredswarts55 commented 4 years ago

@KTibow vlc is pretty good about continuously reading from the disk and doing its best to watch the video stream, when I ended it, vlc stopped like the video was over.

KTibow commented 4 years ago

@jaredswarts55 here's your code modified a little (so it streams a certain amount):

import asyncio, time

from aiohttp import ClientSession
from simplipy import *

async def get_subscriptions(user_id: str, access_token: str, client: ClientSession):
    url = 'https://api.simplisafe.com/v1/users/{}/subscriptions'.format(user_id)
    async with client.get(url, headers={'Authorization': 'Bearer {}'.format(access_token)}) as resp:
        return await resp.json()

async def get_camera_feed(camera_id: str, access_token: str, client: ClientSession, capture_time: int):
    start_time = time.time()
    url = 'https://media.simplisafe.com/v1/{}/flv?x=472&audioEncoding=AAC'.format(camera_id)
    with open('testing.flv', 'wb') as file:
        async with client.get(url, headers={'Authorization': 'Bearer {}'.format(access_token)}) as resp:
            async for data in resp.content.iter_chunked(1024):
                file.write(data)
                if time.time() - capture_time > start_time:
                    print("Captured "+str(capture_time)+" seconds of video, exiting.")
                    break

async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as session:
        simplisafe = await API.login_via_credentials(
            input("What's your email: "), input("What's your password: "), session=session
        )

        systems = await simplisafe.get_systems();
        for s in systems:
            system = systems[s]

        subscriptionsBody = await get_subscriptions(simplisafe.user_id, simplisafe.access_token, session)
        cameraId = subscriptionsBody["subscriptions"][0]["location"]["system"]["cameras"][0]["uuid"]
        await get_camera_feed(cameraId, simplisafe.access_token, session, int(input("How many seconds?")))

asyncio.run(main())

Sorry if I forgot to put await somewhere, I don't know asyncio programming that well.

uberlinuxguy commented 4 years ago

This is pretty darned cool. And just to add some inspiration here, I modified the script that @jaredswarts55 posted to output the text to stderr and the data to stdout. I was able to pipe that to ffmpeg which would then connect to an ffserver to rebroadcast the stream in a meaninful way, and connect that stream into ZoneMinder. This has the potential to give you the ability to stream and store your videos on your own machine, but sadly it still requires you to have a subscription for the camera.

One major drawback, the SimpliSafe API silently stops sending video after 5 minutes. (4:58 to be more exact). There are potential ways around this issue, like having a small buffer to use while you re-setup the connection, but I haven't gotten to playing with that. It does look like there has to be about a 30 second cool-down on the API before making another request. I really hope I am missing something and that limit can be overcome. @nikonratm Have you seen the 5 minute limit?

KTibow commented 4 years ago

Anyone want to try to analyze the network traffic when you try to stream for 11 minutes and see how it bypasses the 5 minute limit? (Make sure to keep the device awake.)

uberlinuxguy commented 4 years ago

OH! This may not be a simplisafe limit! It appears that this could be a timeout on the aiohttp ClientSession object in python... https://docs.aiohttp.org/en/stable/client_reference.html

KTibow commented 4 years ago

@uberlinuxguy Looks better if you add ?highlight=timeout to the end: https://docs.aiohttp.org/en/stable/client_reference.html?highlight=timeout Looks like we need to pass it a ClientTimeout somewhere: https://docs.aiohttp.org/en/stable/client_reference.html?highlight=timeout#aiohttp.ClientTimeout This should work (I've added in a 24-hour timeout, so just refresh the session every day, just in case it gets stale):

timeout=aiohttp.ClientTimeout(total=60*60*24, connect=None,
                      sock_connect=None, sock_read=None)
uberlinuxguy commented 4 years ago

@KTibow The docs say if you set any or all of the timeouts to 0 or none they are disabled. I am currently running that test to see what happens... 4 mins in...

uberlinuxguy commented 4 years ago

First test with the timeouts set to zero ran until 6:20s. I think my camera may be flaky or something.

I'll keep trying and post my modified code later once I work out the kinks.

Edit: A second test is still running at 12:30s... HOORAY!

shamoon commented 4 years ago

@uberlinuxguy awesome work. The feed is definitely kinda temperamental about long-term streaming, we have the same issues but never looked too deeply into it. One thought would be to consider taking X second chunks to save to disk. Eg this snippet gives 10 second mp4s which can easily be joined back together:

ffmpeg -re -headers "Authorization: Bearer xxx=" -i "https://54.82.224.248/v1/xxx/flv?x=640&audioEncoding=AAC" -vcodec copy -pix_fmt yuv420p -r 20 -b:v 132k -bufsize 264k -maxrate 132k -f segment -segment_time 10 -segment_format_options movflags=+faststart -reset_timestamps 1 segment%d.mp4
KTibow commented 4 years ago
ffmpeg -re -headers "Authorization: Bearer xxx=" -i "https://54.82.224.248/v1/xxx/flv?x=640&audioEncoding=AAC" -vcodec copy -pix_fmt yuv420p -r 20 -b:v 132k -bufsize 264k -maxrate 132k -f segment -segment_time 10 -segment_format_options movflags=+faststart -reset_timestamps 1 segment%d.mp4

Might be doable with ffmpeg-python... we could concat the two files...

KTibow commented 4 years ago

Any updates on this? It sounds doable.

bggardner commented 3 years ago

Like @shamoon, I've implemented camera integration in a PHP script at bggardner/simplisafe-proxy. The whole script takes a very minimalist approach, especially during authentication, as I only pass what seems to be necessary, instead of fully mimicking the WebApp. Feel free to steal ideas or ask questions (though it's been a while since I've looked at it).

stale[bot] commented 3 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

KTibow commented 3 years ago

Go away bot, it's not solved. You're just making unnecessary notifications.

bachya commented 3 years ago

@KTibow This is a valid use case of the bot: it's been over 30 days since this issue was last touched. Let's add the help wanted label.

jhujasonw commented 3 years ago

I had a stream that was "working", but abandoned the idea for a few reasons:

If someone is still interested in working on this, I may be able to dig up the python I was using, but it was a small modification of what is already posted here to stream endlessly to stdout which was redirected to ffmpeg

bachya commented 3 years ago

Quick comment: #182 added initial support for this, but the comments here (@jhujasonw's more recently) are still worth consideration before we can consider this fully closed. So, going to leave this open for further thought and effort.

veedubb65 commented 3 years ago

I have a simplisafe doorbell camera. Is there anything I can do to assist with getting the cameras (especially the doorbell camera) better integrated?

bachya commented 3 years ago

@veedubb65 Read all the above and see where you can help.

jarodwilson commented 2 years ago

Hey @bachya, thanks for your help getting my "refuse to pay for any level of service" simplisafe setup working, and as we discussed, I've got the doorbell with camera that I'd be more than happy to hack on.

nprez83 commented 1 year ago

Hi everyone, wondering if there's been any progress on accessing the Simplisafe Cam stream from HA. I'd love to be able to do that. Thanks @bachya for all the hard work you've put into this awesome integration, definitely one of the most important (if not the most) integrations I have.

peebles commented 8 months ago

For the outdoor camera, in case you haven't seen the timeline response, looks like this:

GET https://api.simplisafe.com/v1/subscriptions/6720379/events?numEvents=50

{
    "numEvents": 50,
    "lastEventTimestamp": 1697547602,
    "events": [
        {
            "eventId": 35952684503,
            "eventTimestamp": 1697598147,
            "eventCid": 1170,
            "zoneCid": "1",
            "sensorType": 17,
            "sensorSerial": "f11b6abd",
            "account": "00499FF8",
            "userId": 5913258,
            "sid": 6720379,
            "info": "Camera Detected Motion",
            "pinName": "",
            "sensorName": "Back Yard",
            "messageSubject": "Camera Detected Motion",
            "messageBody": "Camera Detected Motion on 10/17/2023 at 8:02 PM",
            "eventType": "activityCam",
            "timezone": 3,
            "locationOffset": -420,
            "videoStartedBy": "6172311af9da430ab2e11c59f11b6abd",
            "video": {
                "6172311af9da430ab2e11c59f11b6abd": {
                    "clipId": 24021857466,
                    "preroll": 1,
                    "postroll": 12,
                    "cameraName": "Back Yard",
                    "cameraModel": "SSOBCM4",
                    "aspectRatio": "16:9",
                    "duration": 13,
                    "recordingType": "KVS",
                    "account": "611485993050",
                    "region": "us-east-1",
                    "_links": {
                        "_self": {
                            "href": "https://chronicle.us-east-1.prd.cam.simplisafe.com/v1/recordings/24021857466",
                            "method": "GET"
                        },
                        "preview/mjpg": {
                            "href": "https://remix.us-east-1.prd.cam.simplisafe.com/v1/preview/6172311af9da430ab2e11c59f11b6abd/6720379/time/1697598146/1697598159?account=611485993050&region=us-east-1{&fps,width}",
                            "method": "GET",
                            "templated": true
                        },
                        "snapshot/mjpg": {
                            "href": "https://remix.us-east-1.prd.cam.simplisafe.com/v1/preview/6172311af9da430ab2e11c59f11b6abd/6720379/time/1697598146/1697598150?account=611485993050&region=us-east-1{&fps,width}",
                            "method": "GET",
                            "templated": true
                        },
                        "snapshot/jpg": {
                            "href": "https://remix.us-east-1.prd.cam.simplisafe.com/v1/snapshot/6172311af9da430ab2e11c59f11b6abd/6720379/time/1697598146?account=611485993050&region=us-east-1{&width}",
                            "Method": "GET",
                            "templated": true
                        },
                        "download/mp4": {
                            "href": "https://remix.us-east-1.prd.cam.simplisafe.com/v1/download/6172311af9da430ab2e11c59f11b6abd/6720379/time/1697598146/1697598159?account=611485993050&region=us-east-1",
                            "method": "GET"
                        },
                        "share": {
                            "href": "https://remix.us-east-1.prd.cam.simplisafe.com/v2/share/6172311af9da430ab2e11c59f11b6abd/6720379/time/1697598146/1697598159?account=611485993050&region=us-east-1",
                            "method": "POST"
                        },
                        "playback/dash": {
                            "href": "https://mediator.prd.cam.simplisafe.com/v1/recording/6172311af9da430ab2e11c59f11b6abd/6720379/time/1697598146/1697598159/dash?account=611485993050&region=us-east-1",
                            "method": "GET"
                        },
                        "playback/hls": {
                            "href": "https://mediator.prd.cam.simplisafe.com/v1/recording/6172311af9da430ab2e11c59f11b6abd/6720379/time/1697598146/1697598159/hls?account=611485993050&region=us-east-1",
                            "method": "GET"
                        }
                    }
                }
            }
        },
...

I used curl on the snapshot/jpg url (with the Authorization: Bearer token) and got a good snap from the camera.

mginz83 commented 7 months ago

I don't really know what I am doing in python but I was able to stream the flv to a file and watch it with vlc using this code:

import asyncio

from aiohttp import ClientSession
from simplipy import *

async def get_subscriptions(user_id: str, access_token: str, client: ClientSession):
    url = 'https://api.simplisafe.com/v1/users/{}/subscriptions'.format(user_id)
    print("Url To Send Call To: {}".format(url))
    async with client.get(url, headers={'Authorization': 'Bearer {}'.format(access_token)}) as resp:
        return await resp.json()

async def get_camera_feed(camera_id: str, access_token: str, client: ClientSession):
    url = 'https://media.simplisafe.com/v1/{}/flv?x=472&audioEncoding=AAC'.format(camera_id)
    print("Url To Send Call To: {}".format(url))
    with open('testing.flv', 'wb') as file:
        async with client.get(url, headers={'Authorization': 'Bearer {}'.format(access_token)}) as resp:
            async for data in resp.content.iter_chunked(1024):
                file.write(data)

async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as session:
        simplisafe = await API.login_via_credentials(
            "YOUR_EMAIL_HERE", "YOUR_PASSWORD_HERE", session=session
        )

        systems = await simplisafe.get_systems();
        for s in systems:
            system = systems[s]

        subscriptionsBody = await get_subscriptions(simplisafe.user_id, simplisafe.access_token, session)
        cameraId = subscriptionsBody["subscriptions"][0]["location"]["system"]["cameras"][0]["uuid"]
        print("Camera Id: {}".format(cameraId))
        print("Subscriptions: {}".format(subscriptionsBody))
        await get_camera_feed(cameraId, simplisafe.access_token, session)

asyncio.run(main())

how do we look at the feed? can we call this API from, lets say javascript or react?

I took this URL into a browser and it didnt work... print("Url To Send Call To: {}".format(url))

KTibow commented 7 months ago

I took this URL into a browser and it didnt work

The code should be pretty easily portable if you consider things like sending authorization

Zippyduda commented 7 months ago

I'd love to see this working so I could view the SimpliSafe cameras in Home Assistant (running HAOS on a Raspberry Pi 4) like I do with Hikvision currently. I've got SimpliSafe on the Pro Premium plan (UK) in case anyone needs help testing this to get it working. I'm no coder sadly.

peebles commented 7 months ago

For home assistant support, its not in this repo that changes would need to be made. https://github.com/home-assistant/core.git, under homeassistant/components/simplisafe/, the code in HA that uses this repo.

peebles commented 6 months ago

Locally, I have made modifications to home-assistant/core/homeassistant/components/simplisafe for the OUTDOOR_CAMERA as described below, and I wonder if this is a good approach.

I treat the OUTDOOR_CAMERA as a binary_sensor (motion). This new sensor listens for EVENT_CAMERA_MOTION_DETECTED coming from the Simplisafe websocket. When this event occurs it fetches the "event list" (or timeline) from Simplisafe server and looks for events targeted at the right device serial number, occurring at the same timestamp as the websocket event. These raw SS timeline events contain the media urls for the motion event (see my comment above for the format). I download the snapshot/jpg and download/mp4 files and store them under /config/www/simplisafe//[latest_snapshot.jpg, latest_clip.mp4] and ./clips/YYYYMMDDHHmmss.mp4. I use the HACS gallery card to browse the clips. I use the latest_snaphot.jpg in notification automations.

It does what I want for the OUTDOOR_CAMERA. Its not a camera, in that there is no way to stream the current video ... there does not seem to be a good way of doing this for these camera types.

pcmoore commented 6 months ago

It may not be a full-fledged video feed, but it is much better than what we currently have (nothing). I'm not sure what the HA maintainers think, but I would be excited to have this level of camera support in the SimpliSafe integration.

Does it work for the doorbell too, or just the outdoor camera?

mginz83 commented 6 months ago

Can you share your implementation? Ive tried many ways but cant get this one to work for me?

peebles commented 6 months ago

I'll go through the developer checklist and do a pull request, maybe today.

peebles commented 6 months ago

@mginz83 So you know how to run an integration with changes locally in a running HA? If you do, you'll find my changes in https://github.com/peebles/core/tree/simplisafe-outdoor-motion-capture/homeassistant/components/simplisafe, specifically in binary_sensor.py. Comments in the code will give you some idea how to use it.

I have submitted a PR, but it might take a while to be approved and released.

pcmoore commented 6 months ago

@peebles can you link to the PR here? It would be nice to see a record of all the various attempts in this issue.

KTibow commented 6 months ago

Link: https://github.com/home-assistant/core/pull/106008

blazgocompany commented 3 months ago

I modified @jaredswarts55 solution to use an Auth Code/Code Verifier:


import asyncio

from aiohttp import ClientSession
from simplipy import *

async def get_subscriptions(user_id: str, access_token: str, client: ClientSession):
    url = 'https://api.simplisafe.com/v1/users/{}/subscriptions'.format(user_id)
    print("Url To Send Call To: {}".format(url))
    async with client.get(url, headers={'Authorization': 'Bearer {}'.format(access_token)}) as resp:
        return await resp.json()

async def get_camera_feed(camera_id: str, access_token: str, client: ClientSession):
    url = 'https://media.simplisafe.com/v1/{}/flv?x=472&audioEncoding=AAC'.format(camera_id)
    print("Url To Send Call To: {}".format(url))
    with open('testing.flv', 'wb') as file:
        async with client.get(url, headers={'Authorization': 'Bearer {}'.format(access_token)}) as resp:
            async for data in resp.content.iter_chunked(1024):
                file.write(data)

async def main() -> None:
    """Create the aiohttp session and run."""
    async with ClientSession() as session:
        simplisafe= await API.async_from_auth(
            "BFHQc62Rfb6HdUhbcATWVNffUlNxgWfnSEWank1IldUyP",
            "GWvuu5a22As6EVvGuoLMFkFZqangk2GSqkYSeDRhl52aTB96yg",
            session=session,
        )

        systems = await simplisafe.async_get_systems()
        for s in systems:
            system = systems[s]

        subscriptionsBody = await get_subscriptions(simplisafe.user_id, simplisafe.access_token, session)
        #print("Camera: " + dir(subscriptionsBody["subscriptions"][0]["location"]["system"]["cameras"][0]))

        cameraId = subscriptionsBody["subscriptions"][0]["location"]["system"]["cameras"][0]["uuid"]
        print("Camera Id: {}".format(cameraId))
        print("Subscriptions: {}".format(subscriptionsBody))
        await get_camera_feed(cameraId, simplisafe.access_token, session)

asyncio.run(main())

But it streamed to a flv file with 0 bytes. Any idea why?

KTibow commented 3 months ago

Perhaps the request failed. Check the status of resp.

psbankar commented 1 week ago

Just wanted to check if this now supports simplicam on free subscription. I could not access the camera from my end