-
Notifications
You must be signed in to change notification settings - Fork 128
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
Read all data from the device, disable double-encoding, implement more APIs, refactor querying, update README #11
Conversation
Can you give us a dump of the system info for your plug? You could be having different firmware version than mine which leads to those differences. |
Threre you go, I masked some information with XXXXXes.
edit: made it readable |
I did a quick test (using the cli.py file) and your code seems to be working on both python 2 and 3. I think it'd be also good to bump the version to 0.2.2 in /setup.py and merge this. EDIT: I'm not sure the cli.py should be part of the module, it does help, but it might be better used as an example in the README, rather than a standalone python script. Thanks for sharing the model info. My plug appears to be the exact same hardware and software version like yours. |
I agree about cli.py, although maybe it could be placed into a separate dir like examples, and include a shorter usage example in README.md? Unfortunately I don't have any historical data left as I moved my plug and cleared the device, but if you create a loop for daily you'd probably see the device delivering incomplete packets sometimes. Unfortunately I have no insight what may cause the behavior in #3, the sockets seem to be closed properly, but in case it's not also done on the plug's end, maybe it runs out of file descriptors? One could test that by doing lots of queries and see if it stops being responsive. |
Uhuh, I may have made a mess by working on the same initial branch for new set of patches. If that's okay then fine, if not, I'll have to figure out how to rebase and split these changes. :) edit: looks like I messed up some changes from others... Will have to check that out when I get some time for that. For now this can be put in pending mode. |
Can you please rebase your commits onto the current state of the master branch? |
chunk = sock.recv(4096) | ||
buffer += chunk | ||
#_LOGGER.debug("Got chunk %s" % len(chunk)) | ||
if len(chunk) == 0: |
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.
if not chuck:
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.
Good catch, didn't recall for sure whether empty b'' would've matched that, but it does.
@@ -318,12 +318,20 @@ def query(host, request, port=9999): | |||
|
|||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
sock.connect((host, port)) | |||
_LOGGER.debug("> (%i) %s" % (len(request), request)) |
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.
String interpolation is deprecated, use str.format()
instead.
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.
Done.
sock.shutdown(socket.SHUT_RDWR) | ||
sock.close() | ||
|
||
response = TPLinkSmartHomeProtocol.decrypt(buffer) | ||
response = TPLinkSmartHomeProtocol.decrypt(buffer[4:]) | ||
_LOGGER.debug("< (%i) %s" % (len(response), response)) |
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.
Interpolation here as well.
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.
Done.
logging.info("Sysinfo: %s" % hs.get_sysinfo()) | ||
has_emeter = hs.has_emeter | ||
if has_emeter: | ||
logging.info("== Emeter ==") |
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.
Please indent with 4 spaces.
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.
Done.
logging.basicConfig(level=logging.DEBUG) | ||
|
||
if len(sys.argv) < 2: | ||
print("%s <ip>" % sys.argv[0]) |
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.
Please indent with 4 spaces.
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.
Done.
:return: | ||
""" | ||
keys = ["sw_ver", "hw_ver", "mac", "hwId", "fwId", "oemId", "dev_name"] | ||
return {key:self.sys_info[key] for key in keys} |
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.
add a whitespace after the colon.
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.
Done.
Location of the device, as read from sysinfo | ||
:return: | ||
""" | ||
|
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.
remove the empty line
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.
Done.
Returns WiFi signal strenth (rssi) | ||
:return: rssi | ||
""" | ||
|
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.
remove the empty line
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.
Done.
Sets new mac address | ||
:param mac: mac in hexadecimal with colons, e.g. 01:23:45:67:89:ab | ||
""" | ||
|
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.
remove the empty line
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.
Done.
Content for hash and icon are unknown. | ||
:param icon: | ||
""" | ||
|
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.
remove the empty line
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.
Done.
2b6efd8
to
639d9fa
Compare
Thanks for the review and insightful comments :) I hope I managed to clean-up the mess I made by merging through github in my repo.. |
I wouldn't expect anyone to implement a call to that function when it's
not implemented in the first place.
…On 12/02/2016 08:59 PM, Teemu R. wrote:
***@***.**** commented on this pull request.
------------------------------------------------------------------------
In pyHS100/pyHS100.py <#11>:
> + def icon(self):
+ """
+ Returns device icon
+ Note: this doesn't seem to work when not using the cloud service, not tested with it either.
+ :return:
+ """
+ return self._query_helper("system", "get_dev_icon")
+
+ @icon.setter
+ def icon(self, icon):
+ """
+ Content for hash and icon are unknown.
+ :param icon:
+ """
+
+ raise SmartPlugNotImplementedException()
I was thinking that it allows clients to catch SmartPlugException (as
that inherits from it) without needing separate handling. Does that make
sense to you?
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#11>, or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAICD09Nf_DXFOm-FRXKP1qfN2RheUX7ks5rEHiZgaJpZM4LB0ly>.
|
Agreed. The newest commit simplifies state handling by converting from strings to bools, e.g. A couple of questions:
|
@@ -23,8 +23,6 @@ | |||
_LOGGER = logging.getLogger(__name__) | |||
|
|||
# switch states | |||
SWITCH_STATE_ON = 'on' | |||
SWITCH_STATE_OFF = 'off' |
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.
Let's keep those and let people refer to them, just in case we decide to change the representation again.
else: | ||
_LOGGER.warning("Unknown state %s returned.", relay_state) | ||
_LOGGER.warning("Unknown state %s returned.".formaat(relay_state)) |
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.
formaat
The reason why "ON" and "OFF" are used instead of Booleans is because the module is modeled after the D-Link module handling D-Link's smart plugs. Also from readability stand point if we are to replace ON/OFF with True/False the name of state should be changed to something like |
Yes, we should document Regarding |
HS110 sends sometimes datagrams in chunks especially for get_daystat, this patch makes it to read until there is no more data to be read. As json.dumps() does JSON encoding already, there's no need to str() the year or month either.
This commit adds access to new properties, both read & write, while keeping the old one (mostly) intact. Querying is refactored to be done inside _query_helper() method, which unwraps results automatically and rises SmartPlugException() in case of errors. Errors are to be handled by clients. New features: * Setting device alias (plug.alias = "name") * led read & write * icon read (doesn't seem to return anything without cloud support at least), write API is not known, throws an exception currently * time read (returns datetime), time write implemented, but not working even when no error is returned from the device * timezone read * mac read & write, writing is untested for now. Properties for easier access: * hw_info: return hw-specific elements from sysinfo * on_since: pretty-printed from sysinfo * location: latitude and longitued from sysinfo * rssi: rssi from sysinfo
Following issues are addressed by this commit: * All API is more or less commented (including return types, exceptions, ..) * Converted state to use * Added properties is_on, is_off for those who don't want to check against strings. * Handled most issues reported by pylint. * Adjusted _query_helper() to strip off err_code from the result object. * Fixed broken format() syntax for string formattings.
while True: | ||
chunk = sock.recv(4096) | ||
buffer += chunk | ||
#_LOGGER.debug("Got chunk %s".format(len(chunk))) |
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.
block comment should start with '# '
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.
Really? Can fix though, I don't see it helping much in case of such debug messages, which may sometimes be useful to have. Better strip it off completely?
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.
Yes, better drop it.
:rtype: dict | ||
""" | ||
|
||
return {"latitude": self.sys_info["latitude"], "longitude": self.sys_info["longitude"]} |
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.
line too long (95 > 79 characters)
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.
Fixed.
|
||
if response['err_code'] != 0: | ||
return False | ||
response = self._query_helper("emeter", "get_monthstat", {'year': year}) |
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.
line too long (80 > 79 characters)
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.
Done.
|
||
if response['err_code'] != 0: | ||
return False | ||
response = self._query_helper("emeter", "get_daystat", {'month': month, 'year': year}) |
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.
line too long (94 > 79 characters)
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.
Done.
|
||
result = response[target][cmd] | ||
if result["err_code"] != 0: | ||
raise SmartPlugException("Error on {}.{}: {}".format(target, cmd, result)) |
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.
line too long (86 > 79 characters)
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.
I'd prefer to keep this again for the sake of readability. Moving the call to format to a new line or splitting up the parameter list doesn't really improve reaadability.
|
||
:param target: Target system {system, time, emeter, ..} | ||
:param cmd: Command to execute | ||
:param arg: JSON object passed as parameter to the command, defualts to {} |
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.
line too long (82 > 79 characters)
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.
Also typo "defualts"
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.
Fixed "defualts".
@@ -22,6 +22,11 @@ | |||
|
|||
_LOGGER = logging.getLogger(__name__) | |||
|
|||
class SmartPlugException(Exception): |
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.
expected 2 blank lines, found 1
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.
Done.
@@ -1 +1 @@ | |||
|
|||
from pyHS100.pyHS100 import SmartPlug |
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.
'SmartPlug' imported but unused
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.
False alarm. import pyHS100 allows using pyHS100.SmartPlug as well as from pyHS100 import SmartPlug for library users. Can be reverted though, if from pyHS100.pyHS100 import SmartPlug is more wanted.
def _query_helper(self, target, cmd, arg={}): | ||
""" | ||
Query helper, returns unwrapped result object. | ||
Raises SmartPlugException in case of error reported by the plug. |
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.
Use :raises
and move after last param for consistency.
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.
It's later on there. This is just the descriptive string for it.
Fixed quite a few things, as earlier, commit message contains the changes. Btw, pylint complains that pyHS100 is not a valid name :-) |
:rtype: datetime | ||
""" | ||
return datetime.datetime.now() - \ | ||
datetime.timedelta(seconds=self.sys_info["on_time"]) |
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.
continuation line over-indented for visual indent
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.
Again for readability's sake. If wanted, we could import datetime & timedelta directly and avoid typing datetime twice/at all, if wanted.
""" | ||
response = self._query_helper("time", "get_time") | ||
return datetime.datetime(response["year"], response["month"], response["mday"], | ||
response["hour"], response["min"], response["sec"]) |
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.
line too long (84 > 79 characters)
:raises SmartPlugException: on error | ||
""" | ||
response = self._query_helper("time", "get_time") | ||
return datetime.datetime(response["year"], response["month"], response["mday"], |
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.
line too long (87 > 79 characters)
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.
Please, it's already splitted. Changing response to rsp or r doesn't improve readability.
""" | ||
Returns device icon | ||
|
||
Note: this doesn't seem to work (at least) when not using the cloud service. |
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.
line too long (84 > 79 characters)
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.
Better remove this altogether, if this is an issue. It's for there for devels to see/note that there may be an issue there..
|
||
:param target: Target system {system, time, emeter, ..} | ||
:param cmd: Command to execute | ||
:param arg: JSON object passed as parameter to the command, defaults to {} |
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.
line too long (82 > 79 characters)
Yes, the name should be PyHS100 instead - or at least start with a captical character. But let's keep that for another PR, this one's large enough already. |
Yeah, it's my failure to mess up that much with using proper feature branches, wasn't thinking it would gain so many commits in the end. |
Agreed, unit testing makes total sense. |
On related note, perhaps LED api should also follow the "legacy" ON/OFF for state values, even if I don't really like strings as return values/parameters? |
I agree True/False should be used, not strings. |
:raises SmartPlugException: on error | ||
""" | ||
raise NotImplementedError("Fails with err_code == 0 with HS110.") | ||
""" here just for the sake of completeness / if someone figures out why it doesn't work. |
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.
line too long (88 > 79 characters)
return response['err_code'] == 0 | ||
self.initialize() | ||
|
||
# As query_helper raises exception in case of failure, we have succeeded when we are this far. |
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.
line too long (102 > 79 characters)
|
||
:param target: Target system {system, time, emeter, ..} | ||
:param cmd: Command to execute | ||
:param arg: JSON object passed as parameter to the command, defaults to {} |
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.
line too long (82 > 79 characters)
Adjusted led setter to use True/False instead of ints, fix some styling issues (keep whitespaces in docstrings between headline & other elements. Outed initialization out from init() and force-calling it after all API calls changing the state of the device. I don't really like the initialize solution, maybe sysinfo content shouldbe kept locally and just updated based on responses from the plug. Opinions? |
@GadgetReactor Can we get this merged? |
thanks! |
Thanks for merging, next step is to clean the code base a bit & prepare for easier testing :-) |
The commit messages are pretty self-explanatory, so here's just some extra info: