Skip to content

Commit

Permalink
Remove padding whitespace to satisfy Tado (#182)
Browse files Browse the repository at this point in the history
* Remove padding whitespace to satisfy Tado

Taking iPhone requests as source of truth about the format of json payloads
there should be no whitespace after comma (or anywhere that would be just spacing).
Some devices (like Tado Internet Bridge) are really fussy about that, and sending
json payload in Python's standard formatting will cause it to return error.
I.e. sending perfectly valid json:
    {"characteristics":[{"iid":15, "aid":2, "ev":true}]}
will not work, while:
    {"characteristics":[{"iid":15,"aid":2,"ev":true}]}
is accepted.

* Fixing #181 - strip whitespaces for tado devices

Merging #182
Adding tests for the stripped spaces
Stripping spaces also for put_characteristics

* update contributors list in README

* Compact json payloads

* Extend regression tests

Co-authored-by: Joachim Lusiardi <joachim@lusiardi.de>
  • Loading branch information
elmopl and jlusiardi committed Apr 4, 2020
1 parent 108d6a0 commit cd08284
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 6 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ The code presented in this repository was created based on release R1 from 2017-
* [limkevinkuan](https://github.com/limkevinkuan) [Commits](https://github.com/jlusiardi/homekit_python/commits/master?author=limkevinkuan)
* [tleegaard](https://github.com/tleegaard) [Commits](https://github.com/jlusiardi/homekit_python/commits/master?author=tleegaard)
* [benasse](https://github.com/benasse) [Commits](https://github.com/jlusiardi/homekit_python/commits/master?author=benasse)
* [PaulMcMillan](https://github.com/PaulMcMillan) [Commits](https://github.com/jlusiardi/homekit_python/commits/master?author=PaulMcMillan)
* [elmopl](https://github.com/lmopl) [Commits](https://github.com/jlusiardi/homekit_python/commits/master?author=elmopl)

(The contributors are not listed in any particular order!)

Expand Down
20 changes: 17 additions & 3 deletions homekit/controller/ip_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# limitations under the License.
#

from functools import partial
import json
from json.decoder import JSONDecodeError
import time
Expand All @@ -33,6 +34,19 @@
from homekit.model.services import ServicesTypes


# Taking iPhone requests as source of truth about the format of json payloads
# there should be no whitespace after comma (or anywhere that would be just spacing).
# Some devices (like Tado Internet Bridge) are really fussy about that, and sending
# json payload in Python's standard formatting will cause it to return error.
# I.e. sending perfectly valid json:
# {"characteristics":[{"iid":15, "aid":2, "ev":true}]}
# will not work, while:
# {"characteristics":[{"iid":15,"aid":2,"ev":true}]}
# is accepted.
# See https://github.com/jlusiardi/homekit_python/issues/181
_dump_json = partial(json.dumps, separators=(',', ':'))


class IpPairing(AbstractPairing):
"""
This represents a paired HomeKit IP accessory.
Expand Down Expand Up @@ -227,7 +241,7 @@ def get_resource(self, resource_request):
if not self.session:
self.session = IpSession(self.pairing_data)
url = '/resource'
body = json.dumps(resource_request).encode()
body = _dump_json(resource_request).encode()

try:
response = self.session.post(url, body)
Expand Down Expand Up @@ -276,7 +290,7 @@ def put_characteristics(self, characteristics, do_conversion=False):
value = check_convert_value(value, c_format)
characteristics_set.add('{a}.{i}'.format(a=aid, i=iid))
data.append({'aid': aid, 'iid': iid, 'value': value})
data = json.dumps({'characteristics': data})
data = _dump_json({'characteristics': data})

try:
response = self.session.put('/characteristics', data)
Expand Down Expand Up @@ -329,7 +343,7 @@ def get_events(self, characteristics, callback_fun, max_events=-1, max_seconds=-
iid = characteristic[1]
characteristics_set.add('{a}.{i}'.format(a=aid, i=iid))
data.append({'aid': aid, 'iid': iid, 'ev': True})
data = json.dumps({'characteristics': data})
data = _dump_json({'characteristics': data})

try:
response = self.session.put('/characteristics', data)
Expand Down
112 changes: 109 additions & 3 deletions tests/regression_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,17 @@
"""

import unittest
import json
from unittest import mock

from homekit.controller.ip_implementation import IpPairing
from homekit.http_impl import HomeKitHTTPConnection
from homekit.http_impl.secure_http import SecureHttp
from homekit.protocol import create_ip_pair_setup_write, create_ip_pair_verify_write
from homekit.http_impl.response import HttpResponse


class TestHTTPPairing(unittest.TestCase):

"""
Communication failures in the pairing stage.
Expand Down Expand Up @@ -80,7 +81,6 @@ def test_pair_verify_doesnt_add_extra_headers(self):


class TestSecureSession(unittest.TestCase):

"""
Communication failures of HTTP secure session layer.
Expand All @@ -106,7 +106,6 @@ def test_requests_have_host_header(self):

with mock.patch.object(secure_http, '_handle_request') as handle_req:
secure_http.get('/characteristics')
print(handle_req.call_args[0][0])
assert '\r\nHost: 192.168.1.2:8080\r\n' in handle_req.call_args[0][0].decode()

secure_http.post('/characteristics', b'')
Expand Down Expand Up @@ -135,3 +134,110 @@ def test_requests_only_send_params_for_true_case(self):

pairing.get_characteristics([(1, 2)], include_meta=True)
assert session.get.call_args[0][0] == '/characteristics?id=1.2&meta=1'


class TestMinimalisticJson(unittest.TestCase):
@staticmethod
def function_for_put_testing(url, body):
response = HttpResponse()
response.body = json.dumps({'characteristics': []}).encode()
return response

@classmethod
def prepare_mock_function(cls):
session = mock.Mock()
session.pairing_data = {
'AccessoryIP': '192.168.1.2',
'AccessoryPort': 8080,
'accessories': [
{
"aid": 1,
"services": [
{
"iid": 1,
"type": "0000003E-0000-1000-8000-0026BB765291",
"primary": False,
"hidden": False,
"linked": [],
"characteristics": [
{
"iid": 2,
"type": "00000014-0000-1000-8000-0026BB765291",
"format": "bool",
"perms": [
"pw"
],
"description": "Identify"
},
]
},
]
}
]
}
session.put = mock.Mock(side_effect=cls.function_for_put_testing)
session.post = mock.Mock(side_effect=cls.function_for_put_testing)
return session

def test_get_events_sends_minimal_json(self):
"""
The tado internet bridge will fail if a there are spaces in the json data transmitted to it.
An iPhone client sends minimalistic json with the whitespace stripped out:
b'{"characteristics":[{"aid":2,"iid":12,"ev":true},...,{"aid":2,"iid":17,"ev":true}]}'
https://github.com/jlusiardi/homekit_python/issues/181
https://github.com/jlusiardi/homekit_python/pull/182
"""
session = __class__.prepare_mock_function()

pairing = IpPairing(session.pairing_data)
pairing.session = session

pairing.get_events([(1, 2)], lambda *_: None)

assert session.put.call_args[0][0] == '/characteristics'
payload = session.put.call_args[0][1]
assert ' ' not in payload, 'Regression of https://github.com/jlusiardi/homekit_python/issues/181'

def test_get_resource_sends_minimal_json(self):
"""
The tado internet bridge will fail if a there are spaces in the json data transmitted to it.
An iPhone client sends minimalistic json with the whitespace stripped out:
b'{"characteristics":[{"aid":2,"iid":12,"ev":true},...,{"aid":2,"iid":17,"ev":true}]}'
https://github.com/jlusiardi/homekit_python/issues/181
https://github.com/jlusiardi/homekit_python/pull/182
"""
session = __class__.prepare_mock_function()

pairing = IpPairing(session.pairing_data)
pairing.session = session

pairing.get_resource({'my': 'test', 'for_some': 'resource'})

assert session.post.call_args[0][0] == '/resource'
payload = session.post.call_args[0][1]
assert b' ' not in payload, (payload, 'Regression of https://github.com/jlusiardi/homekit_python/issues/181')

def test_put_characteristics_sends_minimal_json(self):
"""
The tado internet bridge will fail if a there are spaces in the json data transmitted to it.
An iPhone client sends minimalistic json with the whitespace stripped out:
b'{"characteristics":[{"aid":2,"iid":12,"ev":true},...,{"aid":2,"iid":17,"ev":true}]}'
https://github.com/jlusiardi/homekit_python/issues/181
https://github.com/jlusiardi/homekit_python/pull/182
"""
session = __class__.prepare_mock_function()

pairing = IpPairing(session.pairing_data)
pairing.session = session

pairing.put_characteristics([(1, 2, 3)])

assert session.put.call_args[0][0] == '/characteristics'
payload = session.put.call_args[0][1]
assert ' ' not in payload, 'Regression of https://github.com/jlusiardi/homekit_python/issues/181'

0 comments on commit cd08284

Please sign in to comment.