Skip to content

Commit

Permalink
1.1.2 (#61)
Browse files Browse the repository at this point in the history
* Fix disconnect code and add tests

* Update docs and improve error messages

* mock responds with HTTP errors

* Add tests for disconnect

* Fix available not being sent after reconnect

* Test updates to change wait on listen to be the number of events

* Removed py3.5 support
  • Loading branch information
mattsaxon committed Feb 16, 2020
1 parent f64ade4 commit fda62fe
Show file tree
Hide file tree
Showing 14 changed files with 473 additions and 129 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Expand Up @@ -3,7 +3,6 @@ python:
- 3.8
- 3.7
- 3.6
- 3.5
sudo: required
dist: xenial
install: pip install -U tox-travis coveralls
Expand Down
7 changes: 7 additions & 0 deletions HISTORY.rst
@@ -1,6 +1,13 @@
History
=======

1.1.2 (2020-02-15)
------------------

* Fixed issue of reconnection that device remains unavailable until state changes
* Fixed retry code for strip type devices


1.1.1 (2020-02-01)
------------------

Expand Down
2 changes: 1 addition & 1 deletion pysonofflanr3/__init__.py
Expand Up @@ -50,7 +50,7 @@ async def state_callback(device):

__author__ = "Matt Saxon"
__email__ = "saxonmatt@hotmail.com"
__version__ = '1.1.1'
__version__ = "1.1.2"
__url__ = "https://github.com/mattsaxon/pysonofflan"

# flake8: noqa
Expand Down
47 changes: 34 additions & 13 deletions pysonofflanr3/cli.py
Expand Up @@ -9,8 +9,10 @@
from pysonofflanr3 import SonoffSwitch, Discover

if sys.version_info < (3, 5):
print("To use this script you need python 3.5 or newer! got %s"
% sys.version_info)
print(
"To use this script you need python 3.5 or newer! got %s"
% sys.version_info
)
sys.exit(1)


Expand All @@ -31,8 +33,9 @@ def format(self, record):

prefix = self.formatTime(record, self.datefmt) + " - "
if level in self.colors:
prefix += click.style("{}: ".format(level),
**self.colors[level])
prefix += click.style(
"{}: ".format(level), **self.colors[level]
)

msg = "\n".join(prefix + x for x in msg.splitlines())
return msg
Expand Down Expand Up @@ -74,12 +77,18 @@ def format(self, record):
envvar="PYSONOFFLAN_inching",
required=False,
help='Number of seconds of "on" time if this is an '
"Inching/Momentary switch.",
"Inching/Momentary switch.",
)
@click.option(
"--wait",
envvar="PYSONOFFLAN_wait",
required=False,
help="time to wait for listen.",
)
@click.pass_context
@click_log.simple_verbosity_option(logger, "--loglevel", "-l")
@click.version_option()
def cli(ctx, host, device_id, api_key, inching):
def cli(ctx, host, device_id, api_key, inching, wait):
"""A cli tool for controlling Sonoff Smart Switches/Plugs in LAN Mode."""
if ctx.invoked_subcommand == "discover":
return
Expand All @@ -94,6 +103,7 @@ def cli(ctx, host, device_id, api_key, inching):
"device_id": device_id,
"api_key": api_key,
"inching": inching,
"wait": wait,
}


Expand All @@ -105,8 +115,9 @@ def discover():
"on the local network, please wait..."
)
found_devices = (
asyncio.get_event_loop().run_until_complete(
Discover.discover(logger)).items()
asyncio.get_event_loop()
.run_until_complete(Discover.discover(logger))
.items()
)
for found_device_id, ip in found_devices:
logger.debug(
Expand Down Expand Up @@ -157,12 +168,21 @@ def listen(config: dict):
"""Connect to device, print state, then print updates until quit."""

async def state_callback(self):

if self.basic_info is not None:
print_device_details(self)

if self.shared_state["callback_counter"] == 0:
logger.info("Listening for updates forever...' \
'Press CTRL+C to quit.")
logger.info(
"Listening for updates forever...' \
'Press CTRL+C to quit."
)
else:
if config["wait"] is not None:
if self.shared_state["callback_counter"] >= int(
config["wait"]
):
self.shutdown_event_loop()

self.shared_state["callback_counter"] += 1

Expand All @@ -184,15 +204,16 @@ def print_device_details(device):
device_id = device.device_id

logger.info(
click.style("== Device: %s (%s) =="
% (device_id, device.host), bold=True)
click.style(
"== Device: %s (%s) ==" % (device_id, device.host), bold=True
)
)

logger.info(
"State: "
+ click.style(
"ON" if device.is_on else "OFF",
fg="green" if device.is_on else "red"
fg="green" if device.is_on else "red",
)
)

Expand Down
85 changes: 58 additions & 27 deletions pysonofflanr3/client.py
Expand Up @@ -66,6 +66,7 @@ def __init__(
self.type = None
self._info_cache = None
self._last_params = {"switch": "off"}
self._times_added = 0

if self.logger is None:
self.logger = logging.getLogger(__name__)
Expand All @@ -92,6 +93,7 @@ def close_connection(self):
def remove_service(self, zeroconf, type, name):

if self.my_service_name == name:
self._info_cache = None
self.logger.debug("Service %s flagged for removal" % name)
self.loop.run_in_executor(None, self.retry_connection)

Expand All @@ -100,8 +102,15 @@ 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)" % name)
self._times_added += 1
self.logger.info(
"Service %s added again (%s times)"
% (name, self._times_added)
)
self.my_service_name = None
asyncio.run_coroutine_threadsafe(
self.event_handler({}), self.loop
)

if self.my_service_name is None:

Expand Down Expand Up @@ -133,14 +142,15 @@ 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
# (needed for zerconf 0.23.0, 0.24.0, fixed in later versions)
self.service_browser = \
ServiceBrowser(zeroconf, name, listener=self)
self.service_browser = ServiceBrowser(
zeroconf, name, listener=self
)

# create an http session so we can use http keep-alives
self.http_session = requests.Session()
Expand Down Expand Up @@ -178,9 +188,9 @@ def add_service(self, zeroconf, type, name):
method_whitelist=["POST"],
status_forcelist=None,
)
self.http_session.mount("http://", HTTPAdapter(
max_retries=retries)
)
self.http_session.mount(
"http://", HTTPAdapter(max_retries=retries)
)

# process the initial message
self.update_service(zeroconf, type, name)
Expand Down Expand Up @@ -238,36 +248,44 @@ def update_service(self, zeroconf, type, name):
# process the events on an event loop
# this method is on a background thread called from zeroconf
asyncio.run_coroutine_threadsafe(
self.event_handler(data), self.loop)
self.event_handler(data), self.loop
)

except ValueError as ex:
self.logger.error(
"Error updating service for device %s: %s"
" Probably wrong API key.",
self.device_id, format(ex)
self.device_id,
format(ex),
)

asyncio.run_coroutine_threadsafe(
self.event_handler(None), self.loop)
self.event_handler(None), self.loop
)

except TypeError as ex:
self.logger.error(
"Error updating service for device %s: %s"
" Probably missing API key.",
self.device_id, format(ex)
self.device_id,
format(ex),
)

asyncio.run_coroutine_threadsafe(
self.event_handler(None), self.loop)
self.event_handler(None), self.loop
)

except Exception as ex:
self.logger.error(
"Error updating service for device %s: %s, %s",
self.device_id, format(ex), traceback.format_exc()
self.device_id,
format(ex),
traceback.format_exc(),
)

asyncio.run_coroutine_threadsafe(
self.event_handler(None), self.loop)
self.event_handler(None), self.loop
)

def retry_connection(self):

Expand All @@ -278,16 +296,18 @@ def retry_connection(self):
)
self.send_signal_strength()
self.logger.info(
"Service %s not removed (hack worked)" % self.device_id
"Service %s flagged for removal, but is still active!"
% self.device_id
)
break

except OSError as ex:
self.logger.debug(
"Connection issue for device %s: %s",
self.device_id, format(ex)
self.device_id,
format(ex),
)
self.logger.warn("Service %s removed" % self.device_id)
self.logger.info("Service %s removed" % self.device_id)
self.close_connection()
break

Expand All @@ -308,12 +328,12 @@ def send_switch(self, request: Union[str, Dict]):
response = self.send(request, self.url + "/zeroconf/switch")

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

error = response_json["error"]

if error != 0:
self.logger.warn(
self.logger.warning(
"error received: %s, %s", self.device_id, response.content
)
# no need to process error, retry will resend message
Expand All @@ -327,16 +347,25 @@ def send_switch(self, request: Union[str, Dict]):
except Exception as ex:
self.logger.error(
"error %s processing response: %s, %s",
format(ex), response, response.content
format(ex),
response,
response.content,
)

def send_signal_strength(self):

return self.send(
response = self.send(
self.get_update_payload(self.device_id, {}),
self.url + "/zeroconf/signal_strength",
)

if response.status_code == 500:
self.logger.error("500 received")
raise OSError

else:
return response

def send(self, request: Union[str, Dict], url):
"""
Send message to an already-connected Sonoff LAN Mode Device
Expand All @@ -348,16 +377,17 @@ def send(self, request: Union[str, Dict], url):
data = json.dumps(request, separators=(",", ":"))
self.logger.debug("Sending http message to %s: %s", url, data)
response = self.http_session.post(url, data=data)
self.logger.debug("response received: %s %s",
response, response.content)
self.logger.debug(
"response received: %s %s", response, response.content
)

return response

def get_update_payload(self, device_id: str, params: dict) -> Dict:

self._last_params = params

if self.type == b"strip":
if self.type == b"strip" and params != {} and params is not None:

if self.outlet is None:
self.outlet = 0
Expand All @@ -380,7 +410,8 @@ def get_update_payload(self, device_id: str, params: dict) -> Dict:

if self.api_key != "" and self.api_key is not None:
sonoffcrypto.format_encryption_msg(
payload, self.api_key, params)
payload, self.api_key, params
)
self.logger.debug("encrypted: %s", payload)

else:
Expand Down
6 changes: 3 additions & 3 deletions pysonofflanr3/discover.py
Expand Up @@ -51,9 +51,9 @@ def add_service(self, zeroconf, type, name):
device = info.properties[b"id"].decode("ascii")
ip = self.parseAddress(info.address) + ":" + str(info.port)

self.logger.info("Found Sonoff LAN Mode device %s at socket %s"
% (device, ip)
)
self.logger.info(
"Found Sonoff LAN Mode device %s at socket %s" % (device, ip)
)

self.devices[device] = ip

Expand Down
9 changes: 4 additions & 5 deletions pysonofflanr3/sonoffcrypto.py
Expand Up @@ -25,9 +25,7 @@

def format_encryption_msg(payload, api_key, data):

payload[
"selfApikey"
] = "123"
payload["selfApikey"] = "123"
# see https://github.com/itead/Sonoff_Devices_DIY_Tools/issues/5)
iv = generate_iv()
payload["iv"] = b64encode(iv).decode("utf-8")
Expand All @@ -36,8 +34,9 @@ def format_encryption_msg(payload, api_key, data):
if data is None:
payload["data"] = ""
else:
payload["data"] = \
encrypt(json.dumps(data, separators=(",", ":")), iv, api_key)
payload["data"] = encrypt(
json.dumps(data, separators=(",", ":")), iv, api_key
)


def format_encryption_txt(properties, data, api_key):
Expand Down

0 comments on commit fda62fe

Please sign in to comment.