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

FIX "the JSON object must be str, not 'bytes'" error and unexpected c… #1

Open
wants to merge 1 commit into
base: V3-Firmware-fixes
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions pysonofflan/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
class SonoffLANModeClient:
"""
Implementation of the Sonoff LAN Mode Protocol R3(as used by the eWeLink app)

Uses protocol as documented here by Itead https://github.com/itead/Sonoff_Devices_DIY_Tools/blob/master/other/SONOFF%20DIY%20MODE%20Protocol%20Doc.pdf
"""

Expand Down Expand Up @@ -94,9 +94,9 @@ def remove_service(self, zeroconf, type, name):
# this didn't occur on other platforms (Hassio VM).
# I found that sending a HTTP REST message resulted in the device readding itself back on (via add_service) immediately
# rather than waiting for it to occur 'naturally'
# It could be that my Rpi is not picking up all broadcast messages and so the mDNS cache is expiring, but this has not been
# It could be that my Rpi is not picking up all broadcast messages and so the mDNS cache is expiring, but this has not been
# investigated in detail. I would value feedback from other users to see if this 'hack' is called for their setup

self.send_signal_strength(self.get_update_payload(self.device_id, None))
self.logger.debug("Service %s removed (but hack worked)" % name)

Expand All @@ -111,7 +111,7 @@ def remove_service(self, zeroconf, type, name):
def add_service(self, zeroconf, type, name):

if self.my_service_name is not None:

if self.my_service_name == name:
self.logger.debug("Service %s added (again, likely after hack)" % name)
self.my_service_name = None
Expand All @@ -120,7 +120,7 @@ def add_service(self, zeroconf, type, name):
# self.logger.debug("Service %s added (not our switch)" % name)

if self.my_service_name is None:

info = zeroconf.get_service_info(type, name)
found_ip = self.parseAddress(info.address)

Expand All @@ -143,7 +143,7 @@ def add_service(self, zeroconf, type, name):

if self.my_service_name is not None:

self.logger.info("Service type %s of name %s added", type, name)
self.logger.info("Service type %s of name %s added", type, name)

# listen for updates to the specific device
self.service_browser = ServiceBrowser(zeroconf, name, listener=self)
Expand All @@ -154,12 +154,12 @@ def add_service(self, zeroconf, type, name):
# add the http headers
headers = { 'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/json',
'Accept-Language': 'en-gb'
}
'Accept-Language': 'en-gb'
}
self.http_session.headers.update(headers)

# find socket for end-point
socket_text = found_ip + ":" + str(info.port)
socket_text = found_ip + ":" + str(info.port)
self.logger.debug("service is at %s", socket_text)
self.url = 'http://' + socket_text

Expand Down Expand Up @@ -228,11 +228,11 @@ def send(self, request: Union[str, Dict], url):
:param request: command to send to the device (can be dict or json)
:return:
"""
self.logger.debug('Sending http message to %s: %s ', url, request)
self.logger.debug('Sending http message to %s: %s ', url, request)
response = self.http_session.post(url, json=request)
self.logger.debug('response received: %s %s', response, response.content)
self.logger.debug('response received: %s %s', response, response.content)

response_json = json.loads(response.content)
response_json = json.loads(response.content.decode('utf-8'))

error = response_json['error']

Expand All @@ -241,7 +241,7 @@ def send(self, request: Union[str, Dict], url):
# no need to process error, retry will resend message which should be sufficient

else:
self.logger.debug('message sent to switch successfully')
self.logger.debug('message sent to switch successfully')
# no need to do anything here, the update is processed via the mDNS TXT record update


Expand All @@ -262,14 +262,14 @@ def get_update_payload(self, device_id: str, params: dict) -> Dict:
self.format_encryption(payload)
self.logger.debug('encrypted: %s', payload)
else:
self.logger.error('missing api_key field for device: %s', self.device_id)
self.logger.error('missing api_key field for device: %s', self.device_id)

else:
payload["encrypt"] = False

return payload


""" Encrpytion routines as documented in https://github.com/itead/Sonoff_Devices_DIY_Tools/blob/master/other/SONOFF%20DIY%20MODE%20Protocol%20Doc.pdf

Here are an abstract of the document with the partinent parts for the alogrithm
Expand All @@ -295,23 +295,23 @@ def format_encryption(self, data):
data["encrypt"] = encrypt
if encrypt:
iv = self.generate_iv()
data["iv"] = b64encode(iv).decode("utf-8")
data["iv"] = b64encode(iv).decode("utf-8")
data["data"] = self.encrypt(data["data"], iv)


def encrypt(self, data_element, iv):

api_key = bytes(self.api_key, 'utf-8')
api_key = bytes(self.api_key, 'utf-8')
plaintext = bytes(data_element, 'utf-8')

hash = MD5.new()
hash.update(api_key)
key = hash.digest()

cipher = AES.new(key, AES.MODE_CBC, iv=iv)
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
padded = pad(plaintext, AES.block_size)
ciphertext = cipher.encrypt(padded)
encoded = b64encode(ciphertext)
encoded = b64encode(ciphertext)

return encoded.decode("utf-8")

Expand All @@ -332,13 +332,13 @@ def decrypt(self, data_element, iv):
key = hash.digest()

cipher = AES.new(key, AES.MODE_CBC, iv=b64decode(iv))
ciphertext = b64decode(encoded)
ciphertext = b64decode(encoded)
padded = cipher.decrypt(ciphertext)
plaintext = unpad(padded, AES.block_size)
return plaintext

except Exception as ex:
self.logger.error('Error decrypting for device %s: %s, probably wrong API key', self.device_id, format(ex))
self.logger.error('Error decrypting for device %s: %s, probably wrong API key', self.device_id, format(ex))



Expand Down
24 changes: 12 additions & 12 deletions pysonofflan/sonoffdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def __init__(self,
self.params_updated_event = asyncio.Event()

self.client.connect()

self.tasks.append(
self.loop.create_task(self.send_availability_loop()))

Expand Down Expand Up @@ -146,7 +146,7 @@ async def send_updated_params_loop(self):
await self.params_updated_event.wait()

await self.client.connected_event.wait()
self.logger.debug('Connected!')
self.logger.debug('Connected!')

update_message = self.client.get_update_payload(
self.device_id,
Expand All @@ -159,7 +159,7 @@ async def send_updated_params_loop(self):

await self.loop.run_in_executor(None,
self.client.send_switch, update_message)

await asyncio.wait_for(
self.message_ping_event.wait(),
self.calculate_retry(retry_count))
Expand All @@ -174,7 +174,7 @@ async def send_updated_params_loop(self):
"we didn't get a confirmed acknowledgement, "
"state has changed in between retry!")
retry_count += 1

except asyncio.TimeoutError:
self.logger.warn(
'Device: %s. Update message not received in timeout period, retry', self.device_id)
Expand Down Expand Up @@ -227,7 +227,7 @@ async def handle_message(self, message):

self.message_ping_event.set()

response = json.loads(message)
response = json.loads(message.decode('utf-8'))

if ('switch' in response):
self.logger.debug(
Expand All @@ -241,29 +241,29 @@ async def handle_message(self, message):

# is there is a new message queued to be sent
if self.params_updated_event.is_set():

# only send client update message if the change has been successful
if self.params['switch'] == response['switch']:

self.message_acknowledged_event.set()
send_update = True
self.logger.debug('expected update received from switch: %s',
response['switch'])

else:
else:
self.logger.info(
'failed update! state is: %s, expecting: %s',
response['switch'], self.params['switch'])

else:
else:
# this is a status update message originating from the device
# only send client update message if the status has changed

self.logger.info(
'unsolicited update received from switch: %s',
response['switch'])

if self.params['switch'] != response['switch']:
if self.params['switch'] != response['switch']:
self.params = {"switch": response['switch']}
send_update = True

Expand All @@ -274,8 +274,8 @@ async def handle_message(self, message):
self.logger.error(
'Unknown message received from device: ' % message)
raise Exception('Unknown message received from device')


def shutdown_event_loop(self):
self.logger.debug('shutdown_event_loop called')

Expand Down