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

Issue connecting to the plug (rev A1, firmware 1.25) #22

Closed
hcoohb opened this issue Aug 20, 2017 · 12 comments
Closed

Issue connecting to the plug (rev A1, firmware 1.25) #22

hcoohb opened this issue Aug 20, 2017 · 12 comments

Comments

@hcoohb
Copy link
Contributor

hcoohb commented Aug 20, 2017

Hi,
First, thanks for creating that code!

I am trying to setup (to put in Home assistant) the plug recently bought (Australian plug) it is rev A1 and it is on firmware 1.25.
It is working fine in the dlink app, however, when running the python scripts, it throws errors 95% of the time:

Traceback (most recent call last): File "./testw215.py", line 9, in <module> sp = SmartPlug('192.168.1.243', '258798',use_legacy_protocol=True) File "/home/hcooh/hass/lib/python3.5/site-packages/pyW215/pyW215.py", line 60, in __init__ self.model_name = self.SOAPAction(Action="GetDeviceSettings", responseElement="ModelName", params = "") File "/home/hcooh/hass/lib/python3.5/site-packages/pyW215/pyW215.py", line 127, in SOAPAction auth = self.auth() File "/home/hcooh/hass/lib/python3.5/site-packages/pyW215/pyW215.py", line 302, in auth Challenge = root.find('.//{http://purenetworks.com/HNAP1/}Challenge').text AttributeError: 'NoneType' object has no attribute 'text'

it seems the plug does not return expected response...
Surprisingly, it does work from time to time (seems very random to me and not very often)
Are you aware of this issue ? Is there any possible fix?
Happy to help debug if necessary, but just not too sure what to try now.

Thanks a lot
Fabien

@LinuxChristian
Copy link
Owner

Hi Fabien,

Thanks for the bug report!
It seems to be a problem with the authentication step. I would be very interested in knowing what the plug returns so we can get your plug working. On line 299 I extract the response from the initial handshake with the plug. The code looks like this

298        xmlData = response.read().decode()
299        root = ET.fromstring(xmlData)
300
301        # Find responses
302        Challenge = root.find('.//{http://purenetworks.com/HNAP1/}Challenge').text
303        Cookie = root.find('.//{http://purenetworks.com/HNAP1/}Cookie').text
304        Publickey = root.find('.//{http://purenetworks.com/HNAP1/}PublicKey').text

Could you add in a print(root) in the space above. I do not know which platform you are on (Win, Mac, Linux) or your python version, but the file you need to edit on your system is called pyW215.py.

If you could get me the reponse when the plugs works and one where it does not work we can take it from there.

@hcoohb
Copy link
Contributor Author

hcoohb commented Aug 21, 2017

Thank you for the quick answer,

I got it to print the response, here it is when it is failing:

<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><LoginResponse xmlns="http://purenetworks.com/HNAP1/"><LoginResult>ERROR</LoginResult></LoginResponse></soap:Body></soap:Envelope>
Traceback (most recent call last):
  File "./testw215.py", line 9, in <module>
    sp = SmartPlug('192.168.1.243', '258798',use_legacy_protocol=True)
  File "/home/hcooh/hass/lib/python3.5/site-packages/pyW215/pyW215.py", line 60, in __init__
    self.model_name = self.SOAPAction(Action="GetDeviceSettings", responseElement="ModelName", params = "")
  File "/home/hcooh/hass/lib/python3.5/site-packages/pyW215/pyW215.py", line 127, in SOAPAction
    auth = self.auth()
  File "/home/hcooh/hass/lib/python3.5/site-packages/pyW215/pyW215.py", line 303, in auth
    Challenge = root.find('.//{http://purenetworks.com/HNAP1/}Challenge').text
AttributeError: 'NoneType' object has no attribute 'text'

And when it is succeeding:

<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><LoginResponse xmlns="http://purenetworks.com/HNAP1/"><LoginResult>OK</LoginResult><Challenge>dG0M0UpzQ1EvBWglp9Rj</Challenge><Cookie>yJzTRQZVwI</Cookie><PublicKey>xzYXmUd88lecuvudmGWl</PublicKey></LoginResponse></soap:Body></soap:Envelope>
<Element '{http://schemas.xmlsoap.org/soap/envelope/}Envelope' at 0x7fe102979548>
0.0
<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><LoginResponse xmlns="http://purenetworks.com/HNAP1/"><LoginResult>OK</LoginResult><Challenge>BooKMyjaEQlcEZynz1gT</Challenge><Cookie>j8GRPc3agt</Cookie><PublicKey>DKoZUhMpelf5LrQIgem2</PublicKey></LoginResponse></soap:Body></soap:Envelope>
<Element '{http://schemas.xmlsoap.org/soap/envelope/}Envelope' at 0x7fe102904d18>
0
N/A
<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><LoginResponse xmlns="http://purenetworks.com/HNAP1/"><LoginResult>OK</LoginResult><Challenge>BooKMyjaEQlcEZynz1gT</Challenge><Cookie>j8GRPc3agt</Cookie><PublicKey>DKoZUhMpelf5LrQIgem2</PublicKey></LoginResponse></soap:Body></soap:Envelope>
<Element '{http://schemas.xmlsoap.org/soap/envelope/}Envelope' at 0x7fe102904ef8>

For reference I am using the following pyhton script:

import logging,sys
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
sp = SmartPlug('IP', 'pin',use_legacy_protocol=True)
print(sp.current_consumption)
print(sp.temperature)
print(sp.total_consumption)
sp.state = ON

And I realise that pretty much always the first time I am running the script after reconnecting the plug, it runs fine, but from the second time, it does not anymore...
If I wait for some time >10min, it is going to work once again for one run.

Let me know if I can test more!

@hcoohb
Copy link
Contributor Author

hcoohb commented Aug 21, 2017

It seems as well that that new firmware 1.25B03 from the 20 July 17 implements:

Add the back off time to avoid bruce-force attack

according to the release notes... Can this have something to do with the behaviour of the plug?
source: http://support.dlink.com/ProductInfo.aspx?m=DSP-W215

@LinuxChristian
Copy link
Owner

That is interesting. Might be the back off time. 10 min does however seem a long time to wait. I am guessing you can get updates more frequently than 10 min using the app?
In the release notes they do also mention that push events are now fixed. Perhaps they have changed from a pull to a push based model?

I will try to update one of my plugs and see what happens.

@hcoohb
Copy link
Contributor Author

hcoohb commented Aug 23, 2017

Hi, I have done some more investigations and found a possible way forward! A few points:

  1. It seems that only 4 SOAP request with auth() can be sent and answered every ~6min
  2. Now, I modified the code to keep reusing the auth param after the first one, And it works !!!!
    i modified pyW215 as such:
def SOAPAction(self, Action, responseElement, params = ""):
        # Authenticate client
        if self.auth1 is None:
            self.auth1=self.auth()
        auth = self.auth1

But therefore I have to keep the pyW215 object alive in my own script:

#!python3
from pyW215.pyW215 import SmartPlug, ON, OFF
import logging,sys
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
sp = SmartPlug('IP', 'PIN',use_legacy_protocol=True)
# Where ****** is the "code pin" printed on the setup card
while True:
    print("1=on 2=off 3=cons 4=temp 5=tot-cons")
    inp = input("action?")
    if inp=="1":
        sp.state = ON
    elif inp =="2":
        sp.state = OFF
    elif inp=="3":
        print(sp.current_consumption)
    elif inp=="4":
        print(sp.temperature)
    elif inp=="5":
        print(sp.total_consumption)
    else:
        quit()

Now the issue is going to be to retain the auth() results somewhere if we don't wont to keep the script alive indefinitely.
There might be as well a time after which auth() has to be run again. I have not find yet.

Where do you think we can go from here ?
Thanks

@hcoohb
Copy link
Contributor Author

hcoohb commented Aug 24, 2017

Further to yesterday testing,
It seems there are no expiration of the authentication.
-Once it has been done once, no need to do it again until the plug is disconnected from the power!
-It does not impact using the plug from within the app either.

When the plug has been disconnected from power, and then reconnected, if we try to use the old auth() the following error shows up:
WARNING:pyW215.pyW215:Failed to open url to 192.168.x.x
(even though the IP reported is correct)
So I suppose that could be a trigger to ask for re-authentication

And with this kind of modification, there is no need to modify the Home Assistant plugin component as it keeps the object alive through the session

@LinuxChristian
Copy link
Owner

Great you got it working!

I would simply add a class attribute and do something similar to what you did,

def __init__(self, ip, password, user = "admin",
                 use_legacy_protocol = False):
        .....
        self.authenticated = None
        
def SOAPAction(self, Action, responseElement, params = ""):
      ...
        # Authenticate client
        if self.authenticated is None:
            self.authenticated = self.auth()
        auth = self.authenticated

       ...

I think that is a nice solution. If there was a timeout we could also add a self.authenticated_time = time.time() but since that is not the case let's skip that for now.

About the re-authentication. I expect it to fail during init since that has a SOAPAction call. Then you could do something like this,

def __init__(self, ip, password, user = "admin",
                 use_legacy_protocol = False):
        .....
       self.model_name = self.SOAPAction(Action="GetDeviceSettings", responseElement="ModelName", params = "")
       if  self.model_name is None:
                self.model_name = self.reauthenticate(Action="GetDeviceSettings", responseElement="ModelName", params = "")

def reauthenticate(self, action, responseElement, params):
              """
              some informative docstring
              """
                _LOGGER.warning(" Attempting to re-authenticate with plug {}".format(self.ip))
               self.authenticated = None
               response = self.SOAPAction(action, responseElement, params)
              if response is not None:
                    _LOGGER.warning(" Re-authenticate was a success. Now connected to {}".format(self.ip))
              else:
                    _LOGGER.warning(" Re-authenticate failed. Please check the plug {} is turned on".format(self.ip))
              return response

I hacked this together in github so it may not run but you get the idea.

I can do a update with this but since you have the plug to test on (and I think you should have the credit for this) can you make me a pull request for this? Then I will build the package ready for HA.

@hcoohb
Copy link
Contributor Author

hcoohb commented Aug 24, 2017

Thanks for the answer!
I will give it a try to create a PR (not very familiar with it yet, but I need to get started on it, haha).
Just 2 points where I would need your input:

  • Should we do that single Authentication only for the legacy protocol ?
  • I think if we need to re-authenticate, it is not necessarily during init: I am thinking in case of HA, the object is created when HA starts, so if I unplug and replug without restarting HA, the next time HA try to action the plug, it will not work as it needs to reauthenticate. We can maybe just do the test inside the SOAPAction()..?

I will give it a go and submit for feedback
Thanks!

@LinuxChristian
Copy link
Owner

LinuxChristian commented Aug 24, 2017

  • I think using the same scheme for both legacy and normal protols would be best. But I do not know if old plugs have a timeout for authentication. So perhaps it is best to split it as you have already done in the RP.
  • Perhaps a recursive call? Then we do not have to repeat code
   def SOAPAction(self, Action, responseElement, params = "", recursive = False):
       ......
        try:
            response = urlopen(Request(self.url, payload.encode(), headers))
        except (HTTPError, URLError):
            # Try to re-authenticate
            self.authenticated = None
            # Recursive call to retry action
           if not recursive:
               return_value = self.SOAPAction(Action, responseElement, params, True)
           if recursive or return_value is None:
            _LOGGER.warning("Failed to open url to {}".format(self.ip))
            self._error_report = True
            return None
          else:
            return return_value

@LinuxChristian
Copy link
Owner

Submitted version bump for HASS

home-assistant/core#9252

@LinuxChristian
Copy link
Owner

Merged with HASS.

@hcoohb
Copy link
Contributor Author

hcoohb commented Sep 1, 2017

Awesome.
Thank you very much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants