From 5484961a53725bc0a8238cbdece8ccb0d0830ca6 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 10 Mar 2019 18:00:59 +0000 Subject: [PATCH 01/20] Update coverage from 4.5.2 to 4.5.3 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index daf9caa..306b299 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -4,7 +4,7 @@ wheel==0.33.0 watchdog==0.9.0 flake8==3.7.7 tox==3.7.0 -coverage==4.5.2 +coverage==4.5.3 Sphinx==1.8.4 pytest==4.3.0 twine==1.13.0 From a04a3a21badc628bbbf7a9d46f46607ae4a5266b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 20 Mar 2019 03:34:06 +0000 Subject: [PATCH 02/20] Update coveralls from 1.6.0 to 1.7.0 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index daf9caa..8fcc5e2 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -11,7 +11,7 @@ twine==1.13.0 click==7.0 restructuredtext-lint==1.2.2 collective.checkdocs==0.2 -coveralls==1.6.0 +coveralls==1.7.0 pipupgrade==1.4.0 cmarkgfm==0.4.2 mock==2.0.0 From e1e5053d2e4955030c9012159639910ad329d6dd Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 23 Mar 2019 22:23:11 +0000 Subject: [PATCH 03/20] Update restructuredtext-lint from 1.2.2 to 1.3.0 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index daf9caa..5145621 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -9,7 +9,7 @@ Sphinx==1.8.4 pytest==4.3.0 twine==1.13.0 click==7.0 -restructuredtext-lint==1.2.2 +restructuredtext-lint==1.3.0 collective.checkdocs==0.2 coveralls==1.6.0 pipupgrade==1.4.0 From 9bbe85adf5a26e79371a5ab91b8e0e8341508353 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Apr 2019 18:26:48 +0100 Subject: [PATCH 04/20] Update sphinx from 1.8.4 to 2.0.1 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index daf9caa..46d87e4 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -5,7 +5,7 @@ watchdog==0.9.0 flake8==3.7.7 tox==3.7.0 coverage==4.5.2 -Sphinx==1.8.4 +Sphinx==2.0.1 pytest==4.3.0 twine==1.13.0 click==7.0 From 9c676f22d4a8e8290edab8fe2c80916c3cd7aef1 Mon Sep 17 00:00:00 2001 From: mattsaxon Date: Thu, 18 Apr 2019 16:34:30 +0100 Subject: [PATCH 05/20] Draft fix for https://github.com/beveradb/pysonofflan/issues/45 --- pysonofflan/client.py | 5 ++- pysonofflan/sonoffdevice.py | 74 +++++++++++++++++++++++-------------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/pysonofflan/client.py b/pysonofflan/client.py index 23f4719..5037350 100644 --- a/pysonofflan/client.py +++ b/pysonofflan/client.py @@ -121,8 +121,11 @@ async def close_connection(self): self.logger.debug('Closing websocket from client close_connection') self.connected = False if self.websocket is not None: + self.logger.debug('calling websocket.close') await self.websocket.close() - + self.websocket = None # MJS: Ensure we cannot close multiple times + self.logger.debug('websocket was closed') + async def receive_message_loop(self): try: while self.keep_running: diff --git a/pysonofflan/sonoffdevice.py b/pysonofflan/sonoffdevice.py index 7d9db5b..fc32299 100644 --- a/pysonofflan/sonoffdevice.py +++ b/pysonofflan/sonoffdevice.py @@ -75,31 +75,42 @@ def __init__(self, async def setup_connection(self): self.logger.debug('setup_connection is active on the event loop') - try: - self.logger.debug('setup_connection yielding to connect()') - await self.client.connect() - self.logger.debug( - 'setup_connection yielding to send_online_message()') - await self.client.send_online_message() - self.logger.debug( - 'setup_connection yielding to receive_message_loop()') - await self.client.receive_message_loop() - except websockets.InvalidMessage as ex: - self.logger.error('Unable to connect: %s' % ex) - self.shutdown_event_loop() - except ConnectionRefusedError: - self.logger.error('Unable to connect: connection refused') - self.shutdown_event_loop() - except websockets.exceptions.ConnectionClosed: - self.logger.error('Connection closed unexpectedly') - self.shutdown_event_loop() - finally: - self.logger.debug( - 'finally: closing websocket from setup_connection') - await self.client.close_connection() - - self.logger.debug('setup_connection resumed, exiting') - + waits=0 + + while True: + + for tries in range(1,3): # MJS: upto 3 immediate retries before a wait + + try: + self.logger.debug('setup_connection yielding to connect(), try: %i, waits %i', tries, waits) + await self.client.connect() + self.logger.debug( + 'setup_connection yielding to send_online_message()') + await self.client.send_online_message() + self.logger.debug( + 'setup_connection yielding to receive_message_loop()') + await self.client.receive_message_loop() + + except websockets.InvalidMessage as ex: + self.logger.error('Unable to connect: %s' % ex) + break # wait before rety + except ConnectionRefusedError: + self.logger.error('Unable to connect: connection refused') + break # wait before retry + except websockets.exceptions.ConnectionClosed: + self.logger.error('Connection closed unexpectedly in setup_connection') + # retry immediately + finally: + self.logger.debug('finally: closing websocket from setup_connection') + await self.client.close_connection() + + self.logger.debug('waiting 60 seconds before trying again') + await asyncio.sleep(60) + waits+=1 + + self.shutdown_event_loop() # this is causing HA to hang and sometimes not be restartable + self.logger.debug('existing setup_connection()') + async def send_updated_params_loop(self): self.logger.debug( 'send_updated_params_loop is active on the event loop') @@ -116,8 +127,17 @@ async def send_updated_params_loop(self): self.device_id, self.params ) - await self.client.send(update_message) - self.params_updated_event.clear() + self.params_updated_event.clear() # MJS: Clear event before we try send, this is so new event whilst send is blocked is stacked up + + try: + await self.client.send(update_message) + + except websockets.exceptions.ConnectionClosed: # MJS: Added exception handling so to avoid terminating the + self.logger.warn('Connection closed unexpectedly in send()') + + except Exception as ex: + self.logger.error('Unexpected error in send(): %s', format(ex) ) + self.logger.debug('Update message sent, event cleared, should ' 'loop now') finally: From 876280e5da7112f6351231bea3f10c655e1210a6 Mon Sep 17 00:00:00 2001 From: mattsaxon Date: Wed, 24 Apr 2019 12:37:44 +0100 Subject: [PATCH 06/20] Revert "Draft fix for https://github.com/beveradb/pysonofflan/issues/45" This reverts commit 9c676f22d4a8e8290edab8fe2c80916c3cd7aef1. --- pysonofflan/client.py | 5 +-- pysonofflan/sonoffdevice.py | 74 ++++++++++++++----------------------- 2 files changed, 28 insertions(+), 51 deletions(-) diff --git a/pysonofflan/client.py b/pysonofflan/client.py index 5037350..23f4719 100644 --- a/pysonofflan/client.py +++ b/pysonofflan/client.py @@ -121,11 +121,8 @@ async def close_connection(self): self.logger.debug('Closing websocket from client close_connection') self.connected = False if self.websocket is not None: - self.logger.debug('calling websocket.close') await self.websocket.close() - self.websocket = None # MJS: Ensure we cannot close multiple times - self.logger.debug('websocket was closed') - + async def receive_message_loop(self): try: while self.keep_running: diff --git a/pysonofflan/sonoffdevice.py b/pysonofflan/sonoffdevice.py index fc32299..7d9db5b 100644 --- a/pysonofflan/sonoffdevice.py +++ b/pysonofflan/sonoffdevice.py @@ -75,42 +75,31 @@ def __init__(self, async def setup_connection(self): self.logger.debug('setup_connection is active on the event loop') - waits=0 - - while True: - - for tries in range(1,3): # MJS: upto 3 immediate retries before a wait - - try: - self.logger.debug('setup_connection yielding to connect(), try: %i, waits %i', tries, waits) - await self.client.connect() - self.logger.debug( - 'setup_connection yielding to send_online_message()') - await self.client.send_online_message() - self.logger.debug( - 'setup_connection yielding to receive_message_loop()') - await self.client.receive_message_loop() - - except websockets.InvalidMessage as ex: - self.logger.error('Unable to connect: %s' % ex) - break # wait before rety - except ConnectionRefusedError: - self.logger.error('Unable to connect: connection refused') - break # wait before retry - except websockets.exceptions.ConnectionClosed: - self.logger.error('Connection closed unexpectedly in setup_connection') - # retry immediately - finally: - self.logger.debug('finally: closing websocket from setup_connection') - await self.client.close_connection() - - self.logger.debug('waiting 60 seconds before trying again') - await asyncio.sleep(60) - waits+=1 - - self.shutdown_event_loop() # this is causing HA to hang and sometimes not be restartable - self.logger.debug('existing setup_connection()') - + try: + self.logger.debug('setup_connection yielding to connect()') + await self.client.connect() + self.logger.debug( + 'setup_connection yielding to send_online_message()') + await self.client.send_online_message() + self.logger.debug( + 'setup_connection yielding to receive_message_loop()') + await self.client.receive_message_loop() + except websockets.InvalidMessage as ex: + self.logger.error('Unable to connect: %s' % ex) + self.shutdown_event_loop() + except ConnectionRefusedError: + self.logger.error('Unable to connect: connection refused') + self.shutdown_event_loop() + except websockets.exceptions.ConnectionClosed: + self.logger.error('Connection closed unexpectedly') + self.shutdown_event_loop() + finally: + self.logger.debug( + 'finally: closing websocket from setup_connection') + await self.client.close_connection() + + self.logger.debug('setup_connection resumed, exiting') + async def send_updated_params_loop(self): self.logger.debug( 'send_updated_params_loop is active on the event loop') @@ -127,17 +116,8 @@ async def send_updated_params_loop(self): self.device_id, self.params ) - self.params_updated_event.clear() # MJS: Clear event before we try send, this is so new event whilst send is blocked is stacked up - - try: - await self.client.send(update_message) - - except websockets.exceptions.ConnectionClosed: # MJS: Added exception handling so to avoid terminating the - self.logger.warn('Connection closed unexpectedly in send()') - - except Exception as ex: - self.logger.error('Unexpected error in send(): %s', format(ex) ) - + await self.client.send(update_message) + self.params_updated_event.clear() self.logger.debug('Update message sent, event cleared, should ' 'loop now') finally: From aaff04b0edacc703998c12a6bcd7758b60b508e5 Mon Sep 17 00:00:00 2001 From: mattsaxon Date: Thu, 25 Apr 2019 08:18:05 +0100 Subject: [PATCH 07/20] First commit of fully working error handling and retry solution --- pysonofflan/cli.py | 37 +++-- pysonofflan/client.py | 21 +-- pysonofflan/sonoffdevice.py | 290 ++++++++++++++++++++++++++---------- pysonofflan/sonoffswitch.py | 12 +- 4 files changed, 252 insertions(+), 108 deletions(-) diff --git a/pysonofflan/cli.py b/pysonofflan/cli.py index 2734765..d143486 100644 --- a/pysonofflan/cli.py +++ b/pysonofflan/cli.py @@ -143,9 +143,10 @@ def state(config: dict): async def state_callback(device): if device.basic_info is not None: - print_device_details(device) + if device.available: + print_device_details(device) - device.shutdown_event_loop() + device.shutdown_event_loop() logger.info("Initialising SonoffSwitch with host %s" % config['host']) SonoffSwitch( @@ -215,21 +216,27 @@ def switch_device(host, inching, new_state): async def update_callback(device: SonoffSwitch): if device.basic_info is not None: - if inching is None: - logger.info("\nInitial state:") - print_device_details(device) - device.client.keep_running = False - if new_state == "on": - await device.turn_on() - else: - await device.turn_off() - else: - logger.info("Inching device activated by switching ON for " - "%ss" % inching) + if device.available: - logger.info("\nNew state:") - print_device_details(device) + if inching is None: + print_device_details(device) + + if device.is_on: + if new_state == "on": + device.client.keep_running = False + else: + await device.turn_off() + + elif device.is_off: + if new_state == "off": + device.client.keep_running = False + else: + await device.turn_on() + + else: + logger.info("Inching device activated by switching ON for " + "%ss" % inching) SonoffSwitch( host=host, diff --git a/pysonofflan/client.py b/pysonofflan/client.py index 23f4719..cc4e979 100644 --- a/pysonofflan/client.py +++ b/pysonofflan/client.py @@ -4,6 +4,7 @@ import random import time from typing import Dict, Union, Callable, Awaitable +import asyncio import websockets from websockets.framing import OP_CLOSE, parse_close, OP_PING, OP_PONG @@ -80,10 +81,12 @@ def __init__(self, host: str, self.ping_interval = ping_interval self.timeout = timeout self.logger = logger - self.websocket = None self.keep_running = True + self.websocket = None self.event_handler = event_handler - self.connected = False + self.connected_event = asyncio.Event() + self.disconnected_event = asyncio.Event() + if self.logger is None: self.logger = logging.getLogger(__name__) @@ -112,17 +115,20 @@ async def connect(self): subprotocols=['chat'], klass=SonoffLANModeClientProtocol ) - self.connected = True except websockets.InvalidMessage as ex: self.logger.error('SonoffLANModeClient connection failed: %s' % ex) raise ex async def close_connection(self): self.logger.debug('Closing websocket from client close_connection') - self.connected = False + self.connected_event.clear() + self.disconnected_event.set() if self.websocket is not None: + self.logger.debug('calling websocket.close') await self.websocket.close() - + self.websocket = None # Ensure we cannot close multiple times + self.logger.debug('websocket was closed') + async def receive_message_loop(self): try: while self.keep_running: @@ -131,10 +137,7 @@ async def receive_message_loop(self): await self.event_handler(message) self.logger.debug('Message passed to handler, should loop now') finally: - self.logger.debug('receive_message_loop finally block reached: ' - 'closing websocket') - if self.websocket is not None: - await self.websocket.close() + self.logger.debug('receive_message_loop finally block reached') async def send_online_message(self): self.logger.debug('Sending user online message over websocket') diff --git a/pysonofflan/sonoffdevice.py b/pysonofflan/sonoffdevice.py index 7d9db5b..de664c8 100644 --- a/pysonofflan/sonoffdevice.py +++ b/pysonofflan/sonoffdevice.py @@ -7,6 +7,7 @@ import logging from typing import Callable, Awaitable, Dict +import traceback import websockets from .client import SonoffLANModeClient @@ -34,106 +35,210 @@ def __init__(self, self.shared_state = shared_state self.basic_info = None self.params = {} - self.send_updated_params_task = None + self.pending_params = {} + self.old_params = {} # store pending params to be used in retry self.params_updated_event = None self.loop = loop + self.tasks = [] # store the tasks that this module create s in a sequence + self.new_loop = False # use to decide if we should shutdown the loop on exit if logger is None: self.logger = logging.getLogger(__name__) else: self.logger = logger - self.logger.debug( - 'Initializing SonoffLANModeClient class in SonoffDevice') - self.client = SonoffLANModeClient( - host, - self.handle_message, - ping_interval=ping_interval, - timeout=timeout, - logger=self.logger - ) - try: if self.loop is None: + + self.new_loop = True self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) - asyncio.set_event_loop(self.loop) + self.logger.debug( + 'Initializing SonoffLANModeClient class in SonoffDevice') + self.client = SonoffLANModeClient( + host, + self.handle_message, + ping_interval=ping_interval, + timeout=timeout, + logger=self.logger + ) + self.message_received_event = asyncio.Event() self.params_updated_event = asyncio.Event() - self.send_updated_params_task = self.loop.create_task( - self.send_updated_params_loop() - ) - if not self.loop.is_running(): - self.loop.run_until_complete(self.setup_connection()) - else: - asyncio.ensure_future(self.setup_connection()) + self.tasks.append(self.loop.create_task(self.send_updated_params_loop())) + + self.tasks.append(self.loop.create_task(self.send_availability_loop())) + + self.setup_connection_task = self.loop.create_task(self.setup_connection(not self.new_loop)) + self.tasks.append(self.setup_connection_task) + + if self.new_loop: + self.loop.run_until_complete(self.setup_connection_task) except asyncio.CancelledError: self.logger.debug('SonoffDevice loop ended, returning') - async def setup_connection(self): + async def setup_connection(self, retry): self.logger.debug('setup_connection is active on the event loop') + + retry_count = 0 + + while True: + connected = False + try: + self.logger.debug('setup_connection yielding to connect()') + await self.client.connect() + self.logger.debug( + 'setup_connection yielding to send_online_message()') + await self.client.send_online_message() + + connected = True + + except websockets.InvalidMessage as ex: + self.logger.warn('Unable to connect: %s' % ex) + await self.wait_before_retry(retry_count) + except ConnectionRefusedError: + self.logger.warn('Unable to connect: connection refused') + await self.wait_before_retry(retry_count) + except websockets.exceptions.ConnectionClosed: + self.logger.warn('Connection closed unexpectedly during setup') + await self.wait_before_retry(retry_count) + except OSError as ex: + self.logger.warn('OSError in setup_connection(): %s', format(ex) ) + await self.wait_before_retry(retry_count) + except Exception as ex: + self.logger.error('Unexpected error in setup_connection(): %s', format(ex) ) + await self.wait_before_retry(retry_count) + + if connected: + retry_count = 0 # reset retry count after successful connection + try: + self.logger.debug( + 'setup_connection yielding to receive_message_loop()') + await self.client.receive_message_loop() + + except websockets.InvalidMessage as ex: + self.logger.warn('Unable to connect: %s' % ex) + except websockets.exceptions.ConnectionClosed: + self.logger.warn('Connection closed unexpectedly in setup_connection') + except OSError as ex: + self.logger.warn('OSError in receive_message_loop(): %s', format(ex) ) + + except asyncio.CancelledError: + self.logger.debug('receive_message_loop() cancelled' ) + + except Exception as ex: + self.logger.error('Unexpected error in receive_message_loop(): %s', format(ex) ) + + finally: + self.logger.debug('finally: closing websocket from setup_connection') + await self.client.close_connection() + + if not retry: + break + + retry_count +=1 + + self.shutdown_event_loop() + self.logger.debug('exiting setup_connection()') + + async def wait_before_retry(self, retry_count): try: - self.logger.debug('setup_connection yielding to connect()') - await self.client.connect() - self.logger.debug( - 'setup_connection yielding to send_online_message()') - await self.client.send_online_message() - self.logger.debug( - 'setup_connection yielding to receive_message_loop()') - await self.client.receive_message_loop() - except websockets.InvalidMessage as ex: - self.logger.error('Unable to connect: %s' % ex) - self.shutdown_event_loop() - except ConnectionRefusedError: - self.logger.error('Unable to connect: connection refused') - self.shutdown_event_loop() - except websockets.exceptions.ConnectionClosed: - self.logger.error('Connection closed unexpectedly') - self.shutdown_event_loop() - finally: - self.logger.debug( - 'finally: closing websocket from setup_connection') - await self.client.close_connection() - self.logger.debug('setup_connection resumed, exiting') + wait_times = [0.5,1,2,5,10,30,60] + + if retry_count >= len(wait_times): + retry_count = len(wait_times) -1 + + wait_time = wait_times[retry_count] + + self.logger.debug('Waiting %i seconds before retry', wait_time) + + await asyncio.sleep(wait_time) + + except Exception as ex: + self.logger.error('Unexpected error in wait_before_retry(): %s', format(ex) ) + + async def send_availability_loop(self): + + try: + while True: + await self.client.disconnected_event.wait() + + if self.callback_after_update is not None: + await self.callback_after_update(self) + self.client.disconnected_event.clear() + finally: + self.logger.debug('exiting send_availability_loop()') async def send_updated_params_loop(self): self.logger.debug( 'send_updated_params_loop is active on the event loop') try: + self.logger.debug( 'Starting loop waiting for device params to change') - while self.client.keep_running: + while True: self.logger.debug( 'send_updated_params_loop now awaiting event') + await self.params_updated_event.wait() + self.message_received_event.clear() + + await self.client.connected_event.wait() + self.logger.debug('Connected!') + + self.params = self.pending_params + update_message = self.client.get_update_payload( self.device_id, - self.params - ) - await self.client.send(update_message) - self.params_updated_event.clear() - self.logger.debug('Update message sent, event cleared, should ' - 'loop now') - finally: - self.logger.debug( - 'send_updated_params_loop finally block reached: ' - 'closing websocket') - await self.client.close_connection() + self.pending_params + ) - self.logger.debug( - 'send_updated_params_loop resumed outside loop, exiting') + try: + await self.client.send(update_message) + await asyncio.wait_for(self.message_received_event.wait(), 5) + + self.pending_params = {} + self.params_updated_event.clear() + self.logger.debug('Update message sent, event cleared, should ' + 'loop now') + + except websockets.exceptions.ConnectionClosed: + self.logger.error('Connection closed unexpectedly in send()') + except asyncio.TimeoutError: + self.logger.warn('Update message not received, close connection, then loop') + self.params = self.old_params + await self.client.close_connection() # closing connection causes cascade failure in setup_connection and reconnect + except OSError as ex: + self.logger.warn('OSError in send(): %s', format(ex) ) + + except asyncio.CancelledError: + self.logger.debug('send_updated_params_loop cancelled') + + except Exception as ex: + self.logger.error('Unexpected error in send(): %s', format(ex) ) + + except asyncio.CancelledError: + self.logger.debug('send_updated_params_loop cancelled') + + except Exception as ex: + self.logger.error('Unexpected error in send(): %s', format(ex) ) + + finally: + self.logger.debug('send_updated_params_loop finally block reached') def update_params(self, params): self.logger.debug( 'Scheduling params update message to device: %s' % params - ) - self.params = params + ) + self.old_params = self.params + self.pending_params = params self.params_updated_event.set() async def handle_message(self, message): @@ -141,20 +246,32 @@ async def handle_message(self, message): Receive message sent by the device and handle it, either updating state or storing basic device info """ + + self.shared_state = message response = json.loads(message) if ( ('error' in response and response['error'] == 0) and 'deviceid' in response ): + if self.client.connected_event.is_set(): + self.message_received_event.set() # only mark message as accepted if we are already online (otherwise this is an initial connection message) + self.logger.debug( 'Received basic device info, storing in instance') self.basic_info = response + + if self.callback_after_update is not None: + await self.callback_after_update(self) + elif 'action' in response and response['action'] == "update": + self.logger.debug( 'Received update from device, updating internal state to: %s' % response['params']) self.params = response['params'] + self.client.connected_event.set() + self.client.disconnected_event.clear() if self.callback_after_update is not None: await self.callback_after_update(self) @@ -164,10 +281,7 @@ async def handle_message(self, message): raise Exception('Unknown message received from device') def shutdown_event_loop(self): - self.logger.debug( - 'shutdown_event_loop called, setting keep_running to ' - 'False') - self.client.keep_running = False + self.logger.debug('shutdown_event_loop called') try: # Hide Cancelled Error exceptions during shutdown @@ -182,32 +296,43 @@ def shutdown_exception_handler(loop, context): # Handle shutdown gracefully by waiting for all tasks # to be cancelled tasks = asyncio.gather( - *asyncio.all_tasks(loop=self.loop), + *self.tasks, loop=self.loop, return_exceptions=True ) - - tasks.add_done_callback(lambda t: self.loop.stop()) + + if self.new_loop: + tasks.add_done_callback(lambda t: self.loop.stop()) + tasks.cancel() # Keep the event loop running until it is either # destroyed or all tasks have really terminated - while ( - not tasks.done() - and not self.loop.is_closed() - and not self.loop.is_running() - ): - self.loop.run_forever() + + if self.new_loop: + while ( + not tasks.done() + and not self.loop.is_closed() + and not self.loop.is_running() + ): + self.loop.run_forever() + + except Exception as ex: + self.logger.error('Unexpected error in shutdown_event_loop(): %s', format(ex) ) + finally: - if ( - hasattr(self.loop, "shutdown_asyncgens") - and not self.loop.is_running() - ): - # Python 3.5 - self.loop.run_until_complete( - self.loop.shutdown_asyncgens() - ) - self.loop.close() + if self.new_loop: + + if ( + hasattr(self.loop, "shutdown_asyncgens") + and not self.loop.is_running() + ): + # Python 3.5 + self.loop.run_until_complete( + self.loop.shutdown_asyncgens() + ) + self.loop.close() + @property def device_id(self) -> str: @@ -256,3 +381,8 @@ def __repr__(self): return "<%s at %s>" % ( self.__class__.__name__, self.host) + + @property + def available(self) -> bool: + + return self.client.connected_event.is_set() diff --git a/pysonofflan/sonoffswitch.py b/pysonofflan/sonoffswitch.py index 495047a..1d509a0 100644 --- a/pysonofflan/sonoffswitch.py +++ b/pysonofflan/sonoffswitch.py @@ -69,14 +69,17 @@ def state(self) -> str: SWITCH_STATE_UNKNOWN :rtype: str """ - state = self.params['switch'] - + try: + state = self.params['switch'] + except: + state = SonoffSwitch.SWITCH_STATE_UNKNOWN + if state == "off": return SonoffSwitch.SWITCH_STATE_OFF elif state == "on": return SonoffSwitch.SWITCH_STATE_ON else: - self.logger.warning("Unknown state %s returned.", state) + self.logger.debug("Unknown state %s returned.", state) return SonoffSwitch.SWITCH_STATE_UNKNOWN @state.setter @@ -157,11 +160,12 @@ async def pre_callback_after_update(self, _): "Inching switch activated, waiting %ss before " "turning OFF again" % self.inching_seconds) - self.loop.call_later( + inching_task = self.loop.call_later( self.inching_seconds, self.callback_to_turn_off_inching ) + self.tasks.append(inching_task) await self.turn_on() else: self.logger.debug("Not inching switch, calling parent callback") From 0c4c4cd3980a778ab5e95cfca92095c3ddaa1958 Mon Sep 17 00:00:00 2001 From: mattsaxon Date: Thu, 25 Apr 2019 09:26:43 +0100 Subject: [PATCH 08/20] Remove unecessary old_params --- pysonofflan/sonoffdevice.py | 45 ++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/pysonofflan/sonoffdevice.py b/pysonofflan/sonoffdevice.py index de664c8..66d5ed7 100644 --- a/pysonofflan/sonoffdevice.py +++ b/pysonofflan/sonoffdevice.py @@ -35,12 +35,11 @@ def __init__(self, self.shared_state = shared_state self.basic_info = None self.params = {} - self.pending_params = {} - self.old_params = {} # store pending params to be used in retry self.params_updated_event = None self.loop = loop self.tasks = [] # store the tasks that this module create s in a sequence self.new_loop = False # use to decide if we should shutdown the loop on exit + self.messages_received = 0 if logger is None: self.logger = logging.getLogger(__name__) @@ -178,33 +177,35 @@ async def send_updated_params_loop(self): self.logger.debug( 'send_updated_params_loop is active on the event loop') + update_message = None + try: self.logger.debug( 'Starting loop waiting for device params to change') + while True: self.logger.debug( 'send_updated_params_loop now awaiting event') await self.params_updated_event.wait() - - self.message_received_event.clear() await self.client.connected_event.wait() - self.logger.debug('Connected!') + self.logger.debug('Connected!') - self.params = self.pending_params - - update_message = self.client.get_update_payload( - self.device_id, - self.pending_params - ) + if update_message is None: + update_message = self.client.get_update_payload( + self.device_id, + self.params + ) try: + self.message_received_event.clear() await self.client.send(update_message) - await asyncio.wait_for(self.message_received_event.wait(), 5) - self.pending_params = {} + await asyncio.wait_for(self.message_received_event.wait(), 2) + + update_message = None self.params_updated_event.clear() self.logger.debug('Update message sent, event cleared, should ' 'loop now') @@ -213,7 +214,6 @@ async def send_updated_params_loop(self): self.logger.error('Connection closed unexpectedly in send()') except asyncio.TimeoutError: self.logger.warn('Update message not received, close connection, then loop') - self.params = self.old_params await self.client.close_connection() # closing connection causes cascade failure in setup_connection and reconnect except OSError as ex: self.logger.warn('OSError in send(): %s', format(ex) ) @@ -237,8 +237,7 @@ def update_params(self, params): self.logger.debug( 'Scheduling params update message to device: %s' % params ) - self.old_params = self.params - self.pending_params = params + self.params = params self.params_updated_event.set() async def handle_message(self, message): @@ -247,7 +246,8 @@ async def handle_message(self, message): state or storing basic device info """ - self.shared_state = message + self.messages_received +=1 + response = json.loads(message) if ( @@ -258,7 +258,7 @@ async def handle_message(self, message): self.message_received_event.set() # only mark message as accepted if we are already online (otherwise this is an initial connection message) self.logger.debug( - 'Received basic device info, storing in instance') + 'Message: %i: Received basic device info, storing in instance', self.messages_received) self.basic_info = response if self.callback_after_update is not None: @@ -267,9 +267,12 @@ async def handle_message(self, message): elif 'action' in response and response['action'] == "update": self.logger.debug( - 'Received update from device, updating internal state to: %s' - % response['params']) - self.params = response['params'] + 'Message: %i: Received update from device, updating internal state to: %s' + , self.messages_received , response['params'] ) + + if not self.params_updated_event.is_set(): + self.params = response['params'] + self.client.connected_event.set() self.client.disconnected_event.clear() From 7eaf755226b616c189fe616bb9c51c06a57e30d9 Mon Sep 17 00:00:00 2001 From: mattsaxon Date: Fri, 26 Apr 2019 03:09:14 +0100 Subject: [PATCH 09/20] Tidied up closesure of loop in cli --- pysonofflan/cli.py | 6 +++--- pysonofflan/client.py | 3 +-- pysonofflan/sonoffdevice.py | 20 +++++++++++++------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/pysonofflan/cli.py b/pysonofflan/cli.py index d143486..bcf8b37 100644 --- a/pysonofflan/cli.py +++ b/pysonofflan/cli.py @@ -116,7 +116,7 @@ async def device_id_callback(device: SonoffSwitch): if device.basic_info is not None: device.shared_state['device_id_at_current_ip'] = \ device.device_id - device.keep_running = False + device.shutdown_event_loop() SonoffSwitch( host=ip, @@ -224,13 +224,13 @@ async def update_callback(device: SonoffSwitch): if device.is_on: if new_state == "on": - device.client.keep_running = False + device.shutdown_event_loop() else: await device.turn_off() elif device.is_off: if new_state == "off": - device.client.keep_running = False + device.shutdown_event_loop() else: await device.turn_on() diff --git a/pysonofflan/client.py b/pysonofflan/client.py index cc4e979..2887079 100644 --- a/pysonofflan/client.py +++ b/pysonofflan/client.py @@ -81,7 +81,6 @@ def __init__(self, host: str, self.ping_interval = ping_interval self.timeout = timeout self.logger = logger - self.keep_running = True self.websocket = None self.event_handler = event_handler self.connected_event = asyncio.Event() @@ -131,7 +130,7 @@ async def close_connection(self): async def receive_message_loop(self): try: - while self.keep_running: + while True: self.logger.debug('Waiting for messages on websocket') message = await self.websocket.recv() await self.event_handler(message) diff --git a/pysonofflan/sonoffdevice.py b/pysonofflan/sonoffdevice.py index 66d5ed7..31b11d7 100644 --- a/pysonofflan/sonoffdevice.py +++ b/pysonofflan/sonoffdevice.py @@ -121,12 +121,13 @@ async def setup_connection(self, retry): except websockets.InvalidMessage as ex: self.logger.warn('Unable to connect: %s' % ex) except websockets.exceptions.ConnectionClosed: - self.logger.warn('Connection closed unexpectedly in setup_connection') + self.logger.warn('Connection closed in receive_message_loop()') except OSError as ex: self.logger.warn('OSError in receive_message_loop(): %s', format(ex) ) except asyncio.CancelledError: self.logger.debug('receive_message_loop() cancelled' ) + break except Exception as ex: self.logger.error('Unexpected error in receive_message_loop(): %s', format(ex) ) @@ -147,7 +148,7 @@ async def wait_before_retry(self, retry_count): try: - wait_times = [0.5,1,2,5,10,30,60] + wait_times = [0.5,1,2,5,10,30,60] # increasing backoff each retry attempt if retry_count >= len(wait_times): retry_count = len(wait_times) -1 @@ -220,6 +221,7 @@ async def send_updated_params_loop(self): except asyncio.CancelledError: self.logger.debug('send_updated_params_loop cancelled') + break except Exception as ex: self.logger.error('Unexpected error in send(): %s', format(ex) ) @@ -246,7 +248,7 @@ async def handle_message(self, message): state or storing basic device info """ - self.messages_received +=1 + self.messages_received +=1 # ensure debug messages are unique to stop deduplication by logger response = json.loads(message) @@ -255,14 +257,17 @@ async def handle_message(self, message): and 'deviceid' in response ): if self.client.connected_event.is_set(): - self.message_received_event.set() # only mark message as accepted if we are already online (otherwise this is an initial connection message) + self.message_received_event.set() # only mark message as accepted if we are already online (otherwise this is an initial connection message) self.logger.debug( 'Message: %i: Received basic device info, storing in instance', self.messages_received) self.basic_info = response - if self.callback_after_update is not None: - await self.callback_after_update(self) + if self.client.connected_event.is_set(): + self.message_received_event.set() # only mark message as accepted if we are already online (otherwise this is an initial connection message) + + if self.callback_after_update is not None: + await self.callback_after_update(self) elif 'action' in response and response['action'] == "update": @@ -272,12 +277,13 @@ async def handle_message(self, message): if not self.params_updated_event.is_set(): self.params = response['params'] - + self.client.connected_event.set() self.client.disconnected_event.clear() if self.callback_after_update is not None: await self.callback_after_update(self) + else: self.logger.error( 'Unknown message received from device: ' % message) From 945539c6f3704bb193ffa3e09753c91909ca261f Mon Sep 17 00:00:00 2001 From: mattsaxon Date: Fri, 26 Apr 2019 09:01:11 +0100 Subject: [PATCH 10/20] Tidy up message handling and updates --- pysonofflan/sonoffdevice.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/pysonofflan/sonoffdevice.py b/pysonofflan/sonoffdevice.py index 31b11d7..9302a16 100644 --- a/pysonofflan/sonoffdevice.py +++ b/pysonofflan/sonoffdevice.py @@ -178,8 +178,6 @@ async def send_updated_params_loop(self): self.logger.debug( 'send_updated_params_loop is active on the event loop') - update_message = None - try: self.logger.debug( @@ -194,11 +192,10 @@ async def send_updated_params_loop(self): await self.client.connected_event.wait() self.logger.debug('Connected!') - if update_message is None: - update_message = self.client.get_update_payload( - self.device_id, - self.params - ) + update_message = self.client.get_update_payload( + self.device_id, + self.params + ) try: self.message_received_event.clear() @@ -206,7 +203,6 @@ async def send_updated_params_loop(self): await asyncio.wait_for(self.message_received_event.wait(), 2) - update_message = None self.params_updated_event.clear() self.logger.debug('Update message sent, event cleared, should ' 'loop now') @@ -256,15 +252,12 @@ async def handle_message(self, message): ('error' in response and response['error'] == 0) and 'deviceid' in response ): - if self.client.connected_event.is_set(): - self.message_received_event.set() # only mark message as accepted if we are already online (otherwise this is an initial connection message) - self.logger.debug( 'Message: %i: Received basic device info, storing in instance', self.messages_received) self.basic_info = response - if self.client.connected_event.is_set(): - self.message_received_event.set() # only mark message as accepted if we are already online (otherwise this is an initial connection message) + if self.client.connected_event.is_set(): # only mark message as accepted if we are already online (otherwise this is an initial connection message) + self.message_received_event.set() if self.callback_after_update is not None: await self.callback_after_update(self) @@ -275,14 +268,16 @@ async def handle_message(self, message): 'Message: %i: Received update from device, updating internal state to: %s' , self.messages_received , response['params'] ) - if not self.params_updated_event.is_set(): - self.params = response['params'] - self.client.connected_event.set() self.client.disconnected_event.clear() - if self.callback_after_update is not None: - await self.callback_after_update(self) + if not self.params_updated_event.is_set(): # only update internal state if there is not a new message queued to be sent + + if self.params != response['params']: # only send client update message if there is a change + self.params = response['params'] + + if self.callback_after_update is not None: + await self.callback_after_update(self) else: self.logger.error( From 85fef1fa074b843a19b17228052389c767aef80a Mon Sep 17 00:00:00 2001 From: mattsaxon Date: Mon, 29 Apr 2019 10:30:39 +0100 Subject: [PATCH 11/20] Add test for missing hostname in --host --- tests/test_cli.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index 706771e..0dbb036 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -50,6 +50,13 @@ def test_cli_no_device_id(self): result = runner.invoke(cli.cli, ['--device_id']) assert 'Error: --device_id option requires an argument' in \ result.output + + def test_cli_no_host_id(self): + """Test the CLI.""" + runner = CliRunner() + result = runner.invoke(cli.cli, ['--host']) + assert 'Error: --host option requires an argument' in \ + result.output def test_cli_state(self): """Test the CLI.""" From 6b0be3b287810ee855e62c3950058e4cde3caecf Mon Sep 17 00:00:00 2001 From: mattsaxon Date: Mon, 29 Apr 2019 10:36:22 +0100 Subject: [PATCH 12/20] Improve responsiveness to failure When a disconnect error is received when waiting for a reply, this change responds earlier than previous version that always waited 2 seconds to try again. --- pysonofflan/sonoffdevice.py | 39 +++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/pysonofflan/sonoffdevice.py b/pysonofflan/sonoffdevice.py index 9302a16..287bce1 100644 --- a/pysonofflan/sonoffdevice.py +++ b/pysonofflan/sonoffdevice.py @@ -63,7 +63,8 @@ def __init__(self, logger=self.logger ) - self.message_received_event = asyncio.Event() + self.message_ping_event = asyncio.Event() + self.message_acknowledged_event = asyncio.Event() self.params_updated_event = asyncio.Event() self.tasks.append(self.loop.create_task(self.send_updated_params_loop())) @@ -133,6 +134,7 @@ async def setup_connection(self, retry): self.logger.error('Unexpected error in receive_message_loop(): %s', format(ex) ) finally: + self.message_ping_event.set() self.logger.debug('finally: closing websocket from setup_connection') await self.client.close_connection() @@ -198,14 +200,23 @@ async def send_updated_params_loop(self): ) try: - self.message_received_event.clear() + self.message_ping_event.clear() + self.message_acknowledged_event.clear() await self.client.send(update_message) - await asyncio.wait_for(self.message_received_event.wait(), 2) + await asyncio.wait_for(self.message_ping_event.wait(), 2) - self.params_updated_event.clear() - self.logger.debug('Update message sent, event cleared, should ' - 'loop now') + if self.message_acknowledged_event.is_set(): + self.params_updated_event.clear() + self.logger.debug('Update message sent, event cleared, should ' + 'loop now') + else: + self.logger.warn( + "we didn't get an acknowledge message, we have probably been disconnected!") + # message 'ping', but not an acknowledgement, so loop + # if we were disconnected we will wait for reconnection + # if it was another type of message, we will resend change + except websockets.exceptions.ConnectionClosed: self.logger.error('Connection closed unexpectedly in send()') @@ -245,6 +256,7 @@ async def handle_message(self, message): """ self.messages_received +=1 # ensure debug messages are unique to stop deduplication by logger + self.message_ping_event.set() response = json.loads(message) @@ -257,7 +269,7 @@ async def handle_message(self, message): self.basic_info = response if self.client.connected_event.is_set(): # only mark message as accepted if we are already online (otherwise this is an initial connection message) - self.message_received_event.set() + self.message_acknowledged_event.set() if self.callback_after_update is not None: await self.callback_after_update(self) @@ -267,17 +279,20 @@ async def handle_message(self, message): self.logger.debug( 'Message: %i: Received update from device, updating internal state to: %s' , self.messages_received , response['params'] ) - - self.client.connected_event.set() - self.client.disconnected_event.clear() + + if not self.client.connected_event.is_set(): + self.client.connected_event.set() + self.client.disconnected_event.clear() + send_update = True if not self.params_updated_event.is_set(): # only update internal state if there is not a new message queued to be sent if self.params != response['params']: # only send client update message if there is a change self.params = response['params'] + send_update = True - if self.callback_after_update is not None: - await self.callback_after_update(self) + if send_update and self.callback_after_update is not None: + await self.callback_after_update(self) else: self.logger.error( From 006cefb0454eb850cef5f3b92812a181cbee973b Mon Sep 17 00:00:00 2001 From: mattsaxon Date: Mon, 29 Apr 2019 17:09:29 +0100 Subject: [PATCH 13/20] Added Keepalive_pings() method from WS7 for WS6 --- pysonofflan/client.py | 235 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 229 insertions(+), 6 deletions(-) diff --git a/pysonofflan/client.py b/pysonofflan/client.py index 2887079..9dd08fa 100644 --- a/pysonofflan/client.py +++ b/pysonofflan/client.py @@ -5,26 +5,89 @@ import time from typing import Dict, Union, Callable, Awaitable import asyncio +import enum import websockets from websockets.framing import OP_CLOSE, parse_close, OP_PING, OP_PONG +logger = logging.getLogger(__name__) + +V6_DEFAULT_TIMEOUT = 10 +V6_DEFAULT_PING_INTERVAL = 300 + +class InvalidState(Exception): + """ + Exception raised when an operation is forbidden in the current state. + """ + +CLOSE_CODES = { + 1000: "OK", + 1001: "going away", + 1002: "protocol error", + 1003: "unsupported type", + # 1004 is reserved + 1005: "no status code [internal]", + 1006: "connection closed abnormally [internal]", + 1007: "invalid data", + 1008: "policy violation", + 1009: "message too big", + 1010: "extension required", + 1011: "unexpected error", + 1015: "TLS failure [internal]", +} + +# A WebSocket connection goes through the following four states, in order: + +class State(enum.IntEnum): + CONNECTING, OPEN, CLOSING, CLOSED = range(4) + +class ConnectionClosed(InvalidState): + """ + Exception raised when trying to read or write on a closed connection. + Provides the connection close code and reason in its ``code`` and + ``reason`` attributes respectively. + """ + + def __init__(self, code, reason): + self.code = code + self.reason = reason + message = "WebSocket connection is closed: " + message += format_close(code, reason) + super().__init__(message) + +def format_close(code, reason): + """ + Display a human-readable version of the close code and reason. + """ + if 3000 <= code < 4000: + explanation = "registered" + elif 4000 <= code < 5000: + explanation = "private use" + else: + explanation = CLOSE_CODES.get(code, "unknown") + result = "code = {} ({}), ".format(code, explanation) + + if reason: + result += "reason = {}".format(reason) + else: + result += "no reason" + + return result class SonoffLANModeClientProtocol(websockets.WebSocketClientProtocol): """Customised WebSocket client protocol to ignore pong payload match.""" - async def read_data_frame(self, max_size): + @asyncio.coroutine + def read_data_frame(self, max_size): """ Copied from websockets.WebSocketCommonProtocol to change pong handling """ - logger = logging.getLogger(__name__) - while True: - frame = await self.read_frame(max_size) + frame = yield from self.read_frame(max_size) if frame.opcode == OP_CLOSE: self.close_code, self.close_reason = parse_close(frame.data) - await self.write_close_frame(frame.data) + yield from self.write_close_frame(frame.data) return elif frame.opcode == OP_PING: @@ -32,7 +95,7 @@ async def read_data_frame(self, max_size): logger.debug( "%s - received ping, sending pong: %s", self.side, ping_hex ) - await self.pong(frame.data) + yield from self.pong(frame.data) elif frame.opcode == OP_PONG: # Acknowledge pings on solicited pongs, regardless of payload @@ -53,6 +116,166 @@ async def read_data_frame(self, max_size): else: return frame + def __init__(self, **kwds): + + logger.debug("__init__()" ) + + if float(websockets.__version__) < 7.0: + + self.ping_interval = V6_DEFAULT_PING_INTERVAL + self.ping_timeout = V6_DEFAULT_TIMEOUT + + self.close_code: int + self.close_reason: str + + # Task sending keepalive pings. + self.keepalive_ping_task = None + + super().__init__(**kwds) + + def connection_open(self): + + logger.debug("connection_open()") + + super().connection_open() + + if float(websockets.__version__) < 7.0: + + # Start the task that sends pings at regular intervals. + self.keepalive_ping_task = asyncio.ensure_future( + self.keepalive_ping(), loop=self.loop + ) + + @asyncio.coroutine + def keepalive_ping(self): + + logger.debug("keepalive_ping()" ) + + if float(websockets.__version__) >= 7.0: + + super().keepalive_ping() + + else: + + """ + Send a Ping frame and wait for a Pong frame at regular intervals. + This coroutine exits when the connection terminates and one of the + following happens: + - :meth:`ping` raises :exc:`ConnectionClosed`, or + - :meth:`close_connection` cancels :attr:`keepalive_ping_task`. + """ + if self.ping_interval is None: + return + + try: + while True: + + yield from asyncio.sleep(self.ping_interval, loop=self.loop) + + # ping() cannot raise ConnectionClosed, only CancelledError: + # - If the connection is CLOSING, keepalive_ping_task will be + # canceled by close_connection() before ping() returns. + # - If the connection is CLOSED, keepalive_ping_task must be + # canceled already. + + ping_waiter = yield from self.ping() + + if self.ping_timeout is not None: + try: + yield from asyncio.wait_for( + ping_waiter, self.ping_timeout, loop=self.loop + ) + + except asyncio.TimeoutError: + logger.debug("%s ! timed out waiting for pong", self.side) + self.fail_connection(1011) + break + + except asyncio.CancelledError: + raise + + except Exception: + logger.warning("Unexpected exception in keepalive ping task", exc_info=True) + + @asyncio.coroutine + def close_connection(self): + + logger.debug("close_connection()") + + yield from super().close_connection() + + logger.debug("super.close_connection() finished" ) + + if float(websockets.__version__) < 7.0: + + # Cancel the keepalive ping task. + if self.keepalive_ping_task is not None: + self.keepalive_ping_task.cancel() + + + + def abort_keepalive_pings(self): + + logger.debug("abort_keepalive_pings()") + + if float(websockets.__version__) >= 7.0: + super().abort_keepalive_pings() + + else: + + """ + Raise ConnectionClosed in pending keepalive pings. + They'll never receive a pong once the connection is closed. + """ + assert self.state is State.CLOSED + exc = ConnectionClosed(self.close_code, self.close_reason) + exc.__cause__ = self.transfer_data_exc # emulate raise ... from ... + + try: + + for ping in self.pings.values(): + ping.set_exception(exc) + + except asyncio.InvalidStateError: + pass + + """ No Need to do this as in V6, this is done in super.close_connection() + + if self.pings: + pings_hex = ', '.join( + binascii.hexlify(ping_id).decode() or '[empty]' + for ping_id in self.pings + ) + plural = 's' if len(self.pings) > 1 else '' + logger.debug( + "%s - aborted pending ping%s: %s", self.side, plural, pings_hex + )""" + + def connection_lost(self, exc): + + logger.debug("connection_lost()" ) + + if float(websockets.__version__) < 7.0: + + logger.debug("%s - event = connection_lost(%s)", self.side, exc) + self.state = State.CLOSED + logger.debug("%s - state = CLOSED", self.side) + #if not hasattr(self, "close_code"): + self.close_code = 1006 + #if not hasattr(self, "close_reason"): + self.close_reason = "" + logger.debug( + "%s x code = %d, reason = %s", + self.side, + self.close_code, + self.close_reason or "[no reason]", + ) + + self.abort_keepalive_pings() + + super().connection_lost(exc) + + class SonoffLANModeClient: """ From 3c8d3a38bd398bcd1038b0fabab5923c1954f4ad Mon Sep 17 00:00:00 2001 From: mattsaxon Date: Tue, 30 Apr 2019 08:38:46 +0100 Subject: [PATCH 14/20] Fix Py3.5 compile issue --- pysonofflan/client.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pysonofflan/client.py b/pysonofflan/client.py index 9dd08fa..92b48f4 100644 --- a/pysonofflan/client.py +++ b/pysonofflan/client.py @@ -125,8 +125,8 @@ def __init__(self, **kwds): self.ping_interval = V6_DEFAULT_PING_INTERVAL self.ping_timeout = V6_DEFAULT_TIMEOUT - self.close_code: int - self.close_reason: str + #self.close_code: int + #self.close_reason: str # Task sending keepalive pings. self.keepalive_ping_task = None @@ -260,10 +260,10 @@ def connection_lost(self, exc): logger.debug("%s - event = connection_lost(%s)", self.side, exc) self.state = State.CLOSED logger.debug("%s - state = CLOSED", self.side) - #if not hasattr(self, "close_code"): - self.close_code = 1006 - #if not hasattr(self, "close_reason"): - self.close_reason = "" + if self.close_code is None: + self.close_code = 1006 + if self.close_reason is None: + self.close_reason = "" logger.debug( "%s x code = %d, reason = %s", self.side, From 72aca6aa81089fb6fa0c115716cdf0393ac0b220 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 2 May 2019 09:52:39 +0100 Subject: [PATCH 15/20] Update mocket from 2.6.0 to 2.7.3 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 83d2ab9..86b9974 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -15,4 +15,4 @@ coveralls==1.6.0 pipupgrade==1.4.0 cmarkgfm==0.4.2 mock==2.0.0 -mocket==2.6.0 +mocket==2.7.3 From 2e01f0bac087e3a9f77fcc5652eef67b5e0cc081 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 6 May 2019 17:19:38 +0100 Subject: [PATCH 16/20] Update pip from 19.1 to 19.1.1 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 83d2ab9..f0a942b 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,4 +1,4 @@ -pip==19.1 +pip==19.1.1 bump2version==0.5.10 wheel==0.33.0 watchdog==0.9.0 From 526247968e39c95030a780369ecd91910852e0b7 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 7 May 2019 23:22:41 +0100 Subject: [PATCH 17/20] Update mock from 2.0.0 to 3.0.5 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 83d2ab9..ad15f3b 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -14,5 +14,5 @@ collective.checkdocs==0.2 coveralls==1.6.0 pipupgrade==1.4.0 cmarkgfm==0.4.2 -mock==2.0.0 +mock==3.0.5 mocket==2.6.0 From 9a845edbcd12ccdca1c3f1f271384677be105b2b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 9 May 2019 22:09:52 +0100 Subject: [PATCH 18/20] Update pipupgrade from 1.4.0 to 1.5.0 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 83d2ab9..2a88874 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -12,7 +12,7 @@ click==7.0 restructuredtext-lint==1.2.2 collective.checkdocs==0.2 coveralls==1.6.0 -pipupgrade==1.4.0 +pipupgrade==1.5.0 cmarkgfm==0.4.2 mock==2.0.0 mocket==2.6.0 From e8b554d1dfcfd5dc942a5fe5fea45c68bd8946a8 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 12 May 2019 00:16:59 +0100 Subject: [PATCH 19/20] Update wheel from 0.33.0 to 0.33.4 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 83d2ab9..b074545 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,6 +1,6 @@ pip==19.1 bump2version==0.5.10 -wheel==0.33.0 +wheel==0.33.4 watchdog==0.9.0 flake8==3.7.7 tox==3.7.0 From 6fd48507424b9444c05be94be80b88862849ca12 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 12 May 2019 00:17:07 +0100 Subject: [PATCH 20/20] Update pytest from 4.4.1 to 4.5.0 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 83d2ab9..613509f 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -6,7 +6,7 @@ flake8==3.7.7 tox==3.7.0 coverage==4.5.2 Sphinx==1.8.4 -pytest==4.4.1 +pytest==4.5.0 twine==1.13.0 click==7.0 restructuredtext-lint==1.2.2