Skip to content

Commit

Permalink
Merge pull request #7 from amelchio/reconnect
Browse files Browse the repository at this point in the history
Initial error handling
  • Loading branch information
amelchio authored Sep 22, 2018
2 parents 2457dc2 + f827b75 commit c9c8381
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 133 deletions.
2 changes: 1 addition & 1 deletion eternalegypt/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .eternalegypt import Modem
from .eternalegypt import Modem, Error
253 changes: 141 additions & 112 deletions eternalegypt/eternalegypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,38 @@
import logging
import re
import json
import datetime
from functools import wraps
from datetime import datetime
import asyncio
from aiohttp.client_exceptions import ClientError
import async_timeout
import attr

TIMEOUT = 3

_LOGGER = logging.getLogger(__name__)


class Error(Exception):
"""Base class for all exceptions."""


@attr.s
class SMS:
"""An SMS message."""
id = attr.ib()
timestamp = attr.ib()
unread = attr.ib()
sender = attr.ib()
message = attr.ib()


@attr.s
class Information:
"""Various information from the modem."""
sms = attr.ib(factory=list)
usage = attr.ib(default=None)
upstream = attr.ib(default=None) # possible values seem to be WAN and LTE
upstream = attr.ib(default=None)
serial_number = attr.ib(default=None)
connection = attr.ib(default=None)
connection_text = attr.ib(default=None)
Expand All @@ -36,26 +48,50 @@ class Information:
current_band = attr.ib(default=None)
cell_id = attr.ib(default=None)


def autologin(function, timeout=TIMEOUT):
"""Decorator that will try to login and redo an action before failing."""
@wraps(function)
async def wrapper(self, *args, **kwargs):
"""Wrap a function with timeout."""
try:
async with async_timeout.timeout(timeout):
return await function(self, *args, **kwargs)
except (asyncio.TimeoutError, ClientError, Error):
pass

_LOGGER.debug("autologin")
try:
async with async_timeout.timeout(timeout):
await self.login()
return await function(self, *args, **kwargs)
except (asyncio.TimeoutError, ClientError, Error):
raise Error(str(function))

return wrapper


@attr.s
class LB2120:
"""Class for Netgear LB2120 interface."""

hostname = attr.ib()
websession = attr.ib()

token = attr.ib(init=False)
password = attr.ib(default=None)
token = attr.ib(default=None)

listeners = attr.ib(factory=list)
listeners = attr.ib(init=False, factory=list)
max_sms_id = attr.ib(init=False, default=None)
task = attr.ib(init=False, default=None)

@property
def baseurl(self):
def _baseurl(self):
return "http://{}/".format(self.hostname)

def url(self, path):
def _url(self, path):
"""Build a complete URL for the device."""
return self.baseurl + path
return self._baseurl + path

async def add_sms_listener(self, listener):
"""Add a listener for new SMS."""
Expand All @@ -65,99 +101,117 @@ async def logout(self):
"""Cleanup resources."""
self.websession = None
self.token = None
await self.cancel_periodic_update()

async def login(self, password):
async with async_timeout.timeout(10):
url = self.url('index.html')
async with self.websession.get(url) as response:
text = await response.text()
try:
self.token = re.search(r'name="token" value="(.*?)"', text).group(1)
except Exception:
print(response.headers)
_LOGGER.debug("Token: %s", self.token)

url = self.url('Forms/config')
data = {
'session.password': password,
'token': self.token
}
async with self.websession.post(url, data=data) as response:
_LOGGER.debug("Got cookie with status %d", response.status)

await self.periodic_update()

async def login(self, password=None):
"""Create a session with the modem."""
if password is None:
password = self.password
else:
self.password = password

try:
async with async_timeout.timeout(TIMEOUT):
url = self._url('index.html')
async with self.websession.get(url) as response:
text = await response.text()

match = re.search(r'name="token" value="(.*?)"', text)
if not match:
_LOGGER.error("No token found during login")
raise Error()

self.token = match.group(1)
_LOGGER.debug("Token: %s", self.token)

url = self._url('Forms/config')
data = {
'session.password': password,
'token': self.token
}
async with self.websession.post(url, data=data) as response:
_LOGGER.debug("Got cookie with status %d", response.status)

except (asyncio.TimeoutError, ClientError, Error):
raise Error("Could not login")

@autologin
async def sms(self, phone, message):
"""Send a message."""
_LOGGER.debug("Send to %s via %s len=%d",
phone, self.baseurl, len(message))

async with async_timeout.timeout(10):
url = self.url('Forms/smsSendMsg')
data = {
'sms.sendMsg.receiver': phone,
'sms.sendMsg.text': message,
'sms.sendMsg.clientId': __name__,
'action': 'send',
'token': self.token
}
async with self.websession.post(url, data=data) as response:
_LOGGER.debug("Sent message with status %d", response.status)

phone, self._baseurl, len(message))

url = self._url('Forms/smsSendMsg')
data = {
'sms.sendMsg.receiver': phone,
'sms.sendMsg.text': message,
'sms.sendMsg.clientId': __name__,
'action': 'send',
'token': self.token
}
async with self.websession.post(url, data=data) as response:
_LOGGER.debug("Sent message with status %d", response.status)

@autologin
async def delete_sms(self, sms_id):
"""Delete a message."""

async with async_timeout.timeout(10):
url = self.url('Forms/config')
data = {
'sms.deleteId': sms_id,
'err_redirect': '/error.json',
'ok_redirect': '/success.json',
'token': self.token
}
async with self.websession.post(url, data=data) as response:
_LOGGER.debug("Delete %d with status %d", sms_id, response.status)
url = self._url('Forms/config')
data = {
'sms.deleteId': sms_id,
'err_redirect': '/error.json',
'ok_redirect': '/success.json',
'token': self.token
}
async with self.websession.post(url, data=data) as response:
_LOGGER.debug("Delete %d with status %d", sms_id, response.status)

def _build_information(self, data):
"""Read the bits we need from returned data."""
if 'wwan' not in data:
raise Error()

async def information(self):
"""Return the current information."""
result = Information()

async with async_timeout.timeout(10):
url = self.url('model.json')
async with self.websession.get(url) as response:
data = json.loads(await response.text())

result.usage = data['wwan']['dataUsage']['generic']['dataTransferred']
result.upstream = data['failover']['backhaul']
result.serial_number = data['general']['FSN']
result.connection = data['wwan']['connection']
result.connection_text = data['wwan']['connectionText']
result.connection_type = data['wwan']['connectionType']
result.current_nw_service_type = data['wwan']['currentNWserviceType']
result.current_ps_service_type = data['wwan']['currentPSserviceType']
result.register_network_display = data['wwan']['registerNetworkDisplay']
result.roaming = data['wwan']['roaming']
result.radio_quality = data['wwanadv']['radioQuality']
result.rx_level = data['wwanadv']['rxLevel']
result.tx_level = data['wwanadv']['txLevel']
result.current_band = data['wwanadv']['curBand']
result.cell_id = data['wwanadv']['cellId']

for msg in [m for m in data['sms']['msgs'] if 'text' in m]:
# {'id': '6', 'rxTime': '11/03/18 08:18:11 PM', 'text': 'tak tik', 'sender': '555-987-654', 'read': False}
dt = datetime.datetime.strptime(msg['rxTime'], '%d/%m/%y %I:%M:%S %p')
element = SMS(int(msg['id']), dt, not msg['read'], msg['sender'], msg['text'])
result.sms.append(element)
result.sms.sort(key=lambda sms:sms.id)

self.sms_events(result)

await self.periodic_update()
result.usage = data['wwan']['dataUsage']['generic']['dataTransferred']
result.upstream = data['failover']['backhaul']
result.serial_number = data['general']['FSN']
result.connection = data['wwan']['connection']
result.connection_text = data['wwan']['connectionText']
result.connection_type = data['wwan']['connectionType']
result.current_nw_service_type = data['wwan']['currentNWserviceType']
result.current_ps_service_type = data['wwan']['currentPSserviceType']
result.register_network_display = data['wwan']['registerNetworkDisplay']
result.roaming = data['wwan']['roaming']
result.radio_quality = data['wwanadv']['radioQuality']
result.rx_level = data['wwanadv']['rxLevel']
result.tx_level = data['wwanadv']['txLevel']
result.current_band = data['wwanadv']['curBand']
result.cell_id = data['wwanadv']['cellId']

for msg in [m for m in data['sms']['msgs'] if 'text' in m]:
# {'id': '6', 'rxTime': '11/03/18 08:18:11 PM', 'text': 'tak tik',
# 'sender': '555-987-654', 'read': False}
dt = datetime.strptime(msg['rxTime'], '%d/%m/%y %I:%M:%S %p')
element = SMS(int(msg['id']), dt, not msg['read'], msg['sender'], msg['text'])
result.sms.append(element)
result.sms.sort(key=lambda sms: sms.id)

return result

def sms_events(self, information):
@autologin
async def information(self):
"""Return the current information."""
url = self._url('model.json')
async with self.websession.get(url) as response:
data = json.loads(await response.text())

result = self._build_information(data)

self._sms_events(result)

return result

def _sms_events(self, information):
"""Send events for each new SMS."""
if not self.listeners:
return
Expand All @@ -171,31 +225,6 @@ def sms_events(self, information):
if information.sms:
self.max_sms_id = max(s.id for s in information.sms)

async def periodic_update(self):
"""Update information periodically."""
async def update_later():
"""Update information after a delay if we are still current."""
try:
await asyncio.sleep(300)
self.task = None

await self.information()

except asyncio.CancelledError:
pass

await self.cancel_periodic_update()

loop = asyncio.get_event_loop()
self.task = loop.create_task(update_later())

async def cancel_periodic_update(self):
"""Cancel a pending update."""
if self.task:
self.task.cancel()
await asyncio.wait([self.task])
self.task = None


class Modem(LB2120):
"""Class for any modem."""
44 changes: 24 additions & 20 deletions examples/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,30 @@ async def get_information():
jar = aiohttp.CookieJar(unsafe=True)
websession = aiohttp.ClientSession(cookie_jar=jar)

modem = eternalegypt.Modem(hostname=sys.argv[1], websession=websession)
await modem.login(password=sys.argv[2])

result = await modem.information()
print("upstream: {}".format(result.upstream))
print("serial_number: {}".format(result.serial_number))
print("connection: {}".format(result.connection))
print("connection_text: {}".format(result.connection_text))
print("connection_type: {}".format(result.connection_type))
print("current_nw_service_type: {}".format(result.current_nw_service_type))
print("current_ps_service_type: {}".format(result.current_ps_service_type))
print("register_network_display: {}".format(result.register_network_display))
print("roaming: {}".format(result.roaming))
print("radio_quality: {}".format(result.radio_quality))
print("rx_level: {}".format(result.rx_level))
print("tx_level: {}".format(result.tx_level))
print("current_band: {}".format(result.current_band))
print("cell_id: {}".format(result.cell_id))

await modem.logout()
try:
modem = eternalegypt.Modem(hostname=sys.argv[1], websession=websession)
await modem.login(password=sys.argv[2])

result = await modem.information()
print("upstream: {}".format(result.upstream))
print("serial_number: {}".format(result.serial_number))
print("connection: {}".format(result.connection))
print("connection_text: {}".format(result.connection_text))
print("connection_type: {}".format(result.connection_type))
print("current_nw_service_type: {}".format(result.current_nw_service_type))
print("current_ps_service_type: {}".format(result.current_ps_service_type))
print("register_network_display: {}".format(result.register_network_display))
print("roaming: {}".format(result.roaming))
print("radio_quality: {}".format(result.radio_quality))
print("rx_level: {}".format(result.rx_level))
print("tx_level: {}".format(result.tx_level))
print("current_band: {}".format(result.current_band))
print("cell_id: {}".format(result.cell_id))

await modem.logout()
except eternalegypt.Error:
print("Could not login")

await websession.close()

if len(sys.argv) != 3:
Expand Down

0 comments on commit c9c8381

Please sign in to comment.