fronzbot / blinkpy

A Python library for the Blink Camera system
MIT License
570 stars 117 forks source link

Trouble working with 0.16-RC0 - New Login Methods #267

Closed swerb73 closed 4 years ago

swerb73 commented 4 years ago

Hi, I have the new RC up and running but seem to be having trouble getting it to work. I setup a new routine to basically get and generate a new credential file:

from blinkpy.blinkpy import Blink from blinkpy.auth import Auth from blinkpy.helpers.util import json_load

blink = Blink()

auth = Auth(json_load("c:\Blinkpy\cred.json"))

auth = Auth({"username":"MY EMAIL", "password":"MY PASS"}, no_prompt=False) blink.auth = auth blink.start() blink.save("C:\Blinkpy\cred.json")

On first run it works, I get a verification code, plug it in and my cred.json file is generated.

Then I go to run another method to enable/disable a bunch of cameras, like: from blinkpy.blinkpy import Blink from blinkpy.auth import Auth from blinkpy.helpers.util import json_load import time

blink = Blink() auth = Auth(json_load("c:\Blinkpy\cred.json")) blink.auth = auth blink.start() blink.save("C:\Blinkpy\cred.json") <- I also tried to save the cred file at the end, which way shoudl I do it?

blink.cameras["Garage"].set_motion_detect(enable=False) time.sleep(4) blink.cameras["Mud Room"].set_motion_detect(enable=False) time.sleep(4) blink.cameras["Kitchen"].set_motion_detect(enable=False) time.sleep(4) blink.cameras["Dining"].set_motion_detect(enable=False) time.sleep(4) ... A bunch of other cameras.

It may work once but eventually fails on blink.start or on one of the camera commands to disable motion with: C:\Blinkpy>python disable_inside.py Traceback (most recent call last): File "disable_inside.py", line 9, in blink.start() File "C:\Python38\lib\site-packages\blinkpy\blinkpy.py", line 109, in start return self.setup_post_verify() File "C:\Python38\lib\site-packages\blinkpy\blinkpy.py", line 121, in setup_post_verify self.setup_networks() File "C:\Python38\lib\site-packages\blinkpy\blinkpy.py", line 176, in setup_networks response = api.request_networks(self) File "C:\Python38\lib\site-packages\blinkpy\api.py", line 62, in request_networks return http_get(blink, url) File "C:\Python38\lib\site-packages\blinkpy\api.py", line 272, in http_get return blink.auth.query( File "C:\Python38\lib\site-packages\blinkpy\auth.py", line 147, in query return self.validate_response(response, json_resp) File "C:\Python38\lib\site-packages\blinkpy\auth.py", line 114, in validate_response json_data = response.json() File "C:\Python38\lib\site-packages\requests\models.py", line 898, in json return complexjson.loads(self.text, **kwargs) File "C:\Python38\lib\json__init__.py", line 357, in loads return _default_decoder.decode(s) File "C:\Python38\lib\json\decoder.py", line 337, in decode obj, end = self.raw_decode(s, idx=_w(s, 0).end()) File "C:\Python38\lib\json\decoder.py", line 355, in raw_decode raise JSONDecodeError("Expecting value", s, err.value) from None json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Then if I try to regenerate my cred file it will fail with "Cannot setup Blink platform", if I wait a while it eventually works then the process starts all over again, I get a good run, then it fails mid-stream, then I have to wait a while and regen my cred file.

Am I doing something wrong? Should I generate a cred file first (asking for the code from Blink), save the cred file, then reference it in my future runs? Also should I be resaving my cred file after each run?

Thanks for the help!

fronzbot commented 4 years ago

My first thought is you might be getting throttled on Blink's end (they seem to have been more aggressive about temporary account lock outs). 4s is a bit fast to be calling those motion detect enable calls. Try upping that to 10s and see if you still have the same problem.

As for the cred file, there's no harm in generating it again, as long as you're loading that same file every time. Really the only thing that would change is the token if it expires for some reason.

Also, for what it's worth, you could loop on those cameras like this:

cameras_to_arm = ["Garage", "Mud Room", etc]
for name, camera in blink.cameras.items():
  if name in cameras_to_arm:
    camera.set_motion_detect(enable=False)
    time.sleep(10)
swerb73 commented 4 years ago

OK, thanks for the hints, I'll give that a try, I've been trying to tighten it up over time as to avoid false captures but I suppose I can up the time before I call it to give me enough time to cycle through them.

So question, am I doing this right? Basically create my cred file by prompting for the verification code. Then use that cred file in each new script (not prompting for verification), but then just in case save the cred file at the end of the session? And should I save it right after blink.start() (to avoid the script croaking out) or at the end of the script?

fronzbot commented 4 years ago

Yes, that flow is fine. I'd say you're probably better off saving the cred file at the end of the script rather than right after calling blink.start() only because the token used for authentication after login may get refreshed on a subsequent http request. So by saving it at the end you get the last known working token. In theory, it should be identical to the one you used at startup. With that said, since you're running into issues with the script, doing it twice is not be a bad idea.

swerb73 commented 4 years ago

OK, that seems to be helping. I also implemented your loop routine (which simplifies, nice!), the first run work but on my second run it failed somewhere on the inside the loop trying to set motion detect to false. From my app it looks like it failed after the first 2 cameras successful set, then the third failed... Once I have a failure I need to wait a good 2-3m before I try again?

This was the error I got midstream: Traceback (most recent call last): File "disable_inside.py", line 16, in camera.set_motion_detect(enable=False) File "C:\Python38\lib\site-packages\blinkpy\camera.py", line 90, in set_motion_detect return api.request_motion_detection_disable( File "C:\Python38\lib\site-packages\blinkpy\helpers\util.py", line 144, in wrapper result = method(args, kwargs) File "C:\Python38\lib\site-packages\blinkpy\api.py", line 259, in request_motion_detection_disable return http_post(blink, url) File "C:\Python38\lib\site-packages\blinkpy\api.py", line 290, in http_post return blink.auth.query( File "C:\Python38\lib\site-packages\blinkpy\auth.py", line 147, in query return self.validate_response(response, json_resp) File "C:\Python38\lib\site-packages\blinkpy\auth.py", line 114, in validate_response json_data = response.json() File "C:\Python38\lib\site-packages\requests\models.py", line 898, in json return complexjson.loads(self.text, **kwargs) File "C:\Python38\lib\json__init__.py", line 357, in loads return _default_decoder.decode(s) File "C:\Python38\lib\json\decoder.py", line 337, in decode obj, end = self.raw_decode(s, idx=_w(s, 0).end()) File "C:\Python38\lib\json\decoder.py", line 355, in raw_decode raise JSONDecodeError("Expecting value", s, err.value) from None json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Any ideas on how I might try to work around this periodic failure? Can I catch it, wait then try again? Then I start to worry that I'll get back logged on scripts that want to run, one waiting on another... Hmm...

fronzbot commented 4 years ago

Right now you could give this a shot:

import json.decoder.JSONDecodeError

<Bunch of code>
for name, camera in cameras_to_arm:
  ...
  try:
    <disarm device>
  except json.decoder.JSONDecodeError:
    time.sleep(180)
    <Try to disarm again>

That will catch the exception and way 3 minutes before retrying. It's a band-aid fix. Ultimately I'll need to handle this in the library and give you some indication that it failed.

One thing that would be useful to try is once you catch that exception, give the blink.auth.login() endpoint a call and save the result into a variable. If you could copy/paste that response here (removing any sensitive info like client I'd, account I'd, etc) it would help a ton.

swerb73 commented 4 years ago

OK, I'll see if I can capture some of this today. Thanks!

fronzbot commented 4 years ago

Just pushed a new version that should catch this error. Basically what will happen is if you run into it the error should be caught and then a log message written, but your code will continue.

swerb73 commented 4 years ago

OK, thanks, just installed the new version and am running it now. Interestingly I seem to be getting periodic fail messages now:

Expected json response, but received: <Response [403]> Endpoint https://rest-prod.immedia-semi.com/network/XXX/camera/XXX/disable failed Expected json response, but received: <Response [403]> Endpoint https://rest-prod.immedia-semi.com/network/XXX/camera/XXX/disable failed Expected json response, but received: <Response [403]> Endpoint https://rest-prod.immedia-semi.com/network/XXX/camera/XXX/disable failed

NOTE: I replaced the network and camera ID's.

So what is it doing here, is it retrying on it's own and marching through them ok or is it skipping the ones that fail?

I tried to run again right afterwards and got:

Expected json response, but received: <Response [403]> Endpoint https://rest-prod.immedia-semi.com/networks failed Traceback (most recent call last): File "enable_inside.py", line 9, in blink.start() File "C:\Python38\lib\site-packages\blinkpy\blinkpy.py", line 109, in start return self.setup_post_verify() File "C:\Python38\lib\site-packages\blinkpy\blinkpy.py", line 121, in setup_post_verify self.setup_networks() File "C:\Python38\lib\site-packages\blinkpy\blinkpy.py", line 178, in setup_networks self.networks = response["summary"] TypeError: 'NoneType' object is not subscriptable

Also, after a pretty decent day of successful runs yesterday I noticed this morning when I normally disarm about a dozen cameras Blink emailed me another verification code even though I didn't ask for it and was trying to use the cred file? My setup is:

from blinkpy.blinkpy import Blink
from blinkpy.auth import Auth
from blinkpy.helpers.util import json_load
import time

blink = Blink()
auth = Auth(json_load("c:\Blinkpy\cred.json"))
blink.auth = auth
blink.start()

Should I explicitly indicate in the auth line to be silent on the verification code request or is this a sign that I had a failure and why it was trying to get new code?

swerb73 commented 4 years ago

Just noting that after waiting a few minutes I was able to run my script "enable_inside.py" that is noted as completely failing above.

Also I quickly followed it with a disable script that essentially disables the same set of cameras in the same order - I got 5 failures this time and noted that it didn't retry, rather those cameras just never ended up disabled, had to do them manually.

What approach should I take here. Should I note the failure somehow then wait at the end of the script for a few minutes and run again?

fronzbot commented 4 years ago

Yeah those fail messages are expected and what the change I implemented does. I decided handling what to do on those errors should be done on an application-to-application basis since requirements tend to be different.

So what is it doing here, is it retrying on it's own and marching through them ok or is it skipping the ones that fail?

It tries to preform the request (in this case, arm/disarm a camera). That request fails so it tries to refresh the login tokens and perform the request again. Note I am thinking this logic needs to be removed, more on that later. If that fails, it just logs the error and moves on.

Also, after a pretty decent day of successful runs yesterday I noticed this morning when I normally disarm about a dozen cameras Blink emailed me another verification code even though I didn't ask for it and was trying to use the cred file?

My guess is they noticed a bunch of activity on your account and determined killed the token you were using and forced you to re-verify. Arming/disarming seems to be a very sensitive thing for them because I know people who have gotten accounts locked for trying to do that too quickly. Out of curiosity, is there a reason you're trying to arm/disarm individual cameras as opposed to the whole system?

Back to my note

So in the old versions of this library, the tokens would expire after 24hrs requiring you to login again, so logic was added to automatically do that. Now, however, I'm wondering if that's the root cause of all these issues. A request is throttled on the server side, so the library assumes it needs to log in again: a request that's also throttled, increasing the lockout time (or something)? I'm thinking I'll remove that logic altogether and just mark the Blink.available attribute as False so that can be checked and tokens refresh if needed (in fact, I think I already do that in the refresh() routine). Anyways, that's a side note but figured you might find it interesting.

Malarcy commented 4 years ago

Out of curiosity, is there a reason you're trying to arm/disarm individual cameras as opposed to the whole system?

Following this thread - my use case is the same - I have internal and external cameras, including ones in workshops, garage and rear garden - I will manually disable motion sense in those areas when I am working there - but I still want the front of the house and other areas active, and I often forget to reactivate, so I have a script that runs at 8pm to re-activate the motion sense on all the cameras.

fronzbot commented 4 years ago

That makes sense! On that note, I wonder if some of these headaches can be avoided if you first check if motion detection is enabled and then disable? My thought is maybe you're getting throttled if a device you're trying to disable is already disabled and Blink looks at that and says "well, this doesn't make sense".

swerb73 commented 4 years ago

Out of curiosity, is there a reason you're trying to arm/disarm individual cameras as opposed to the whole system?

Following this thread - my use case is the same - I have internal and external cameras, including ones in workshops, garage and rear garden - I will manually disable motion sense in those areas when I am working there - but I still want the front of the house and other areas active, and I often forget to reactivate, so I have a script that runs at 8pm to re-activate the motion sense on all the cameras.

Essentially that's my use case, I'm actually monitoring for events then turning individual cameras on/off based on those events. For example when I open the garage door from the inside I disable the driveway camera so when I pull out of the garage the camera doesn't fire, then when I leave the home geofence the camera is re-armed.

So how would I go about first checking the arm/disarm status? Most of what I do is worst case scenario, assume worst case and turn cameras on/off regardless of state. I like the idea of adding a check to the process... Basically in my loop would I do some sort of if (not armed) then arm?

swerb73 commented 4 years ago

OK, I added some if camera.motion_enabled to my loop, which seemed to work, then I was able to run it 2 times right in a row, on the third run I got: Expected json response, but received: <Response [403]> Endpoint https://rest-prod.immedia-semi.com/network/NETID/camera/CAMID/config failed Could not extract camera info: None Traceback (most recent call last): File "C:\Python38\lib\site-packages\blinkpy\sync_module.py", line 145, in get_camera_info return response["camera"][0] TypeError: 'NoneType' object is not subscriptable Traceback (most recent call last): File "disable_inside.py", line 9, in blink.start() File "C:\Python38\lib\site-packages\blinkpy\blinkpy.py", line 109, in start return self.setup_post_verify() File "C:\Python38\lib\site-packages\blinkpy\blinkpy.py", line 130, in setup_post_verify self.setup_sync_module(name, network_id, sync_cameras) File "C:\Python38\lib\site-packages\blinkpy\blinkpy.py", line 140, in setup_sync_module self.sync[name].start() File "C:\Python38\lib\site-packages\blinkpy\sync_module.py", line 119, in start self.cameras[name].update(camera_info, force_cache=True, force=True) File "C:\Python38\lib\site-packages\blinkpy\camera.py", line 97, in update self.name = config["name"] TypeError: list indices must be integers or slices, not str

fronzbot commented 4 years ago

Could you try that again, but now add this code:

try:
  <your code>
except TypeError:
  print(blink.auth.login())

And let me know what that says (but get rid of sensitive info like tokens and ids). Specifically, there's a "lockout_time" or something like that I'm interested in.

swerb73 commented 4 years ago

Could you try that again, but now add this code:

try:
  <your code>
except TypeError:
  print(blink.auth.login())

And let me know what that says (but get rid of sensitive info like tokens and ids). Specifically, there's a "lockout_time" or something like that I'm interested in.

Should I "try" each line of code or can I wrap that around the entire loop of cameras I'm changing?

fronzbot commented 4 years ago

you can wrap it around the entire loop

swerb73 commented 4 years ago

OK, here's what I got: Expected json response, but received: <Response [403]> Endpoint https://rest-prod.immedia-semi.com/networks failed Traceback (most recent call last): File "disable_inside.py", line 11, in blink.start() File "C:\Python38\lib\site-packages\blinkpy\blinkpy.py", line 109, in start return self.setup_post_verify() File "C:\Python38\lib\site-packages\blinkpy\blinkpy.py", line 121, in setup_post_verify self.setup_networks() File "C:\Python38\lib\site-packages\blinkpy\blinkpy.py", line 178, in setup_networks self.networks = response["summary"] TypeError: 'NoneType' object is not subscriptable

During handling of the above exception, another exception occurred:

Traceback (most recent call last): File "disable_inside.py", line 26, in print(blink.auth.login()) File "C:\Python38\lib\site-packages\blinkpy\auth.py", line 84, in login raise LoginError blinkpy.auth.LoginError

I'm not sure what this means, did the try/catch work? My code:

blink = Blink()
auth = Auth(json_load("c:\Blinkpy\cred.json"))
blink.auth = auth

try:
  blink.start()
  blink.save("C:\Blinkpy\cred.json")

  cameras_to_update = ["Garage", "Mud Room", "Kitchen", "Dining", "Back Porch", "Front Hallway", "Upstairs Bedroom Hallway", "Downstairs Bedroom Hallway"]

  for name, camera in blink.cameras.items():
    if name in cameras_to_update:
      if camera.motion_enabled:
        camera.set_motion_detect(enable=False)
        time.sleep(60)

  if not blink.cameras["Safe"].motion_enabled:
    blink.cameras["Safe"].set_motion_detect(enable=True)

except TypeError:
  print(blink.auth.login())
fronzbot commented 4 years ago

Yeah it worked, but the login endpoint was throttled/blocked so that LoginError was raised. Darn. I guess that means if Blink locks you out, you have no way of knowing when it will expire

Thanks for doing all that, it's all good info to have!

swerb73 commented 4 years ago

OK, so if I'm locked out it appears just waiting a period of time (I've noted 2-3 minutes) opens it back up. Couple thoughts:

Interested in your thoughts here?

You're not seeing these errors in your implementation?

fronzbot commented 4 years ago

Have you thought of a way to create a session that is always logged in (like their app I guess), then passing commands to it that could be executed? Too complex?

Yeah that's the point of the auth handler and loading the credential file. As long as the token you used in your previous session hasn't been invalidated by blink, you can make requests with that token without needing to log back in,

Alternatively what about some way to have a master script (for example I use flask to call these over HTTP) that throttles execution of the scripts. Maybe somehow have flask call a script, have the script pass back some pass/fail, then if fail, wait and try again?

That is something that is better handled on an application-by-application basis rather than in the library. Technically I do have a throttle function on some of the calls, but have it set to a small value. What you're looking for is similar to what home-assistant does (which is where I use this library, personally)

You're not seeing these errors in your implementation?

I do not, but I do not enable/disable individual cameras so my use case is different.

Malarcy commented 4 years ago

One observation.

I have a blink scheduled arm of the system every evening. I assume that’s all done at “blink central”. Sometimes, maybe one in 10 times. I get a message from the blink app saying “arming failed”

Wonder if blink themselves are suffering from there own lockouts.

On 27 May 2020, at 18:33, Kevin Fronczak notifications@github.com wrote:

 Have you thought of a way to create a session that is always logged in (like their app I guess), then passing commands to it that could be executed? Too complex?

Yeah that's the point of the auth handler and loading the credential file. As long as the token you used in your previous session hasn't been invalidated by blink, you can make requests with that token without needing to log back in,

Alternatively what about some way to have a master script (for example I use flask to call these over HTTP) that throttles execution of the scripts. Maybe somehow have flask call a script, have the script pass back some pass/fail, then if fail, wait and try again?

That is something that is better handled on an application-by-application basis rather than in the library. Technically I do have a throttle function on some of the calls, but have it set to a small value. What you're looking for is similar to what home-assistant does (which is where I use this library, personally)

You're not seeing these errors in your implementation?

I do not, but I do not enable/disable individual cameras so my use case is different.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub, or unsubscribe.

swerb73 commented 4 years ago

Yeah that's the point of the auth handler and loading the credential file. As long as the token you used in your previous session hasn't been invalidated by blink, you can make requests with that token without needing to log back in,

So does this mean I don't need to call blink.start() with each new script? Can I somehow fire this up and reuse the credentials file without using blink.start()?

fronzbot commented 4 years ago

@Malarcy - now that would be funny. Given how haphazard their api implementation seems to be, I wouldn't be surprised though.

@swerb73 - The blink.start() method will look at the existing credentials you passed to the Auth class. If the necessary info exists (a token, plus other stuff) it skips the login step entirely and just proceeds with setup. Technically you can call each setup function individually without doing the credential check (see here) but you're not really saving much. As long as you're loading a credential file saved using the blink.save() method, the login step will be skipped as long as the supplied token hasn't been invalidated (a new one will be generated automatically)

swerb73 commented 4 years ago

OK, got it. I'll work with the new RC today and report back. Appears to catch many more of the blink.start() errors internally now. Also switching out my arm/disarm routines. Thanks.

Forced me to refresh my New Device token again this morning, odd.

Otherwise it clearly doesn't want me to run a script too quickly, I need to wait a minute or two, guessing that has something to do with when I call start().