diff options
Diffstat (limited to 'include/py')
-rw-r--r-- | include/py/homekit/config/config.py | 3 | ||||
-rw-r--r-- | include/py/homekit/http/__init__.py | 4 | ||||
-rw-r--r-- | include/py/homekit/http/http.py | 6 | ||||
-rw-r--r-- | include/py/homekit/modem/__init__.py | 3 | ||||
-rw-r--r-- | include/py/homekit/modem/e3372.py | 253 | ||||
-rw-r--r-- | include/py/homekit/util.py | 21 |
6 files changed, 286 insertions, 4 deletions
diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index abdedad..eb2ad82 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -78,6 +78,9 @@ class BaseConfigUnit(ABC): raise KeyError(f'option {key} not found') + def getkeys(self): + return list(self._data.keys()) + class ConfigUnit(BaseConfigUnit): NAME = 'dumb' diff --git a/include/py/homekit/http/__init__.py b/include/py/homekit/http/__init__.py index 6030e95..d019e4c 100644 --- a/include/py/homekit/http/__init__.py +++ b/include/py/homekit/http/__init__.py @@ -1,2 +1,2 @@ -from .http import serve, ok, routes, HTTPServer -from aiohttp.web import FileResponse, StreamResponse, Request, Response +from .http import serve, ok, routes, HTTPServer, HTTPMethod +from aiohttp.web import FileResponse, StreamResponse, Request, Response
\ No newline at end of file diff --git a/include/py/homekit/http/http.py b/include/py/homekit/http/http.py index 3e70751..9b76d9a 100644 --- a/include/py/homekit/http/http.py +++ b/include/py/homekit/http/http.py @@ -1,6 +1,7 @@ import logging import asyncio +from enum import Enum from aiohttp import web from aiohttp.web import Response from aiohttp.web_exceptions import HTTPNotFound @@ -104,3 +105,8 @@ class HTTPServer: def plain(self, text: str): return Response(text=text, content_type='text/plain') + + +class HTTPMethod(Enum): + GET = 'GET' + POST = 'POST' diff --git a/include/py/homekit/modem/__init__.py b/include/py/homekit/modem/__init__.py index 20e75b7..ea0930e 100644 --- a/include/py/homekit/modem/__init__.py +++ b/include/py/homekit/modem/__init__.py @@ -1 +1,2 @@ -from .config import ModemsConfig
\ No newline at end of file +from .config import ModemsConfig +from .e3372 import E3372, MacroNetWorkType diff --git a/include/py/homekit/modem/e3372.py b/include/py/homekit/modem/e3372.py new file mode 100644 index 0000000..f68db5a --- /dev/null +++ b/include/py/homekit/modem/e3372.py @@ -0,0 +1,253 @@ +import requests +import xml.etree.ElementTree as ElementTree + +from ..util import Addr +from enum import Enum +from ..http import HTTPMethod +from typing import Union + + +class Error(Enum): + ERROR_SYSTEM_NO_SUPPORT = 100002 + ERROR_SYSTEM_NO_RIGHTS = 100003 + ERROR_SYSTEM_BUSY = 100004 + ERROR_LOGIN_USERNAME_WRONG = 108001 + ERROR_LOGIN_PASSWORD_WRONG = 108002 + ERROR_LOGIN_ALREADY_LOGIN = 108003 + ERROR_LOGIN_USERNAME_PWD_WRONG = 108006 + ERROR_LOGIN_USERNAME_PWD_ORERRUN = 108007 + ERROR_LOGIN_TOUCH_ALREADY_LOGIN = 108009 + ERROR_VOICE_BUSY = 120001 + ERROR_WRONG_TOKEN = 125001 + ERROR_WRONG_SESSION = 125002 + ERROR_WRONG_SESSION_TOKEN = 125003 + + +class WifiStatus(Enum): + WIFI_CONNECTING = '900' + WIFI_CONNECTED = '901' + WIFI_DISCONNECTED = '902' + WIFI_DISCONNECTING = '903' + + +class Cradle(Enum): + CRADLE_CONNECTING = '900' + CRADLE_CONNECTED = '901' + CRADLE_DISCONNECTED = '902' + CRADLE_DISCONNECTING = '903' + CRADLE_CONNECTFAILED = '904' + CRADLE_CONNECTSTATUSNULL = '905' + CRANDLE_CONNECTSTATUSERRO = '906' + + +class MacroEVDOLevel(Enum): + MACRO_EVDO_LEVEL_ZERO = '0' + MACRO_EVDO_LEVEL_ONE = '1' + MACRO_EVDO_LEVEL_TWO = '2' + MACRO_EVDO_LEVEL_THREE = '3' + MACRO_EVDO_LEVEL_FOUR = '4' + MACRO_EVDO_LEVEL_FIVE = '5' + + +class MacroNetWorkType(Enum): + MACRO_NET_WORK_TYPE_NOSERVICE = 0 + MACRO_NET_WORK_TYPE_GSM = 1 + MACRO_NET_WORK_TYPE_GPRS = 2 + MACRO_NET_WORK_TYPE_EDGE = 3 + MACRO_NET_WORK_TYPE_WCDMA = 4 + MACRO_NET_WORK_TYPE_HSDPA = 5 + MACRO_NET_WORK_TYPE_HSUPA = 6 + MACRO_NET_WORK_TYPE_HSPA = 7 + MACRO_NET_WORK_TYPE_TDSCDMA = 8 + MACRO_NET_WORK_TYPE_HSPA_PLUS = 9 + MACRO_NET_WORK_TYPE_EVDO_REV_0 = 10 + MACRO_NET_WORK_TYPE_EVDO_REV_A = 11 + MACRO_NET_WORK_TYPE_EVDO_REV_B = 12 + MACRO_NET_WORK_TYPE_1xRTT = 13 + MACRO_NET_WORK_TYPE_UMB = 14 + MACRO_NET_WORK_TYPE_1xEVDV = 15 + MACRO_NET_WORK_TYPE_3xRTT = 16 + MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM = 17 + MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO = 18 + MACRO_NET_WORK_TYPE_LTE = 19 + MACRO_NET_WORK_TYPE_EX_NOSERVICE = 0 + MACRO_NET_WORK_TYPE_EX_GSM = 1 + MACRO_NET_WORK_TYPE_EX_GPRS = 2 + MACRO_NET_WORK_TYPE_EX_EDGE = 3 + MACRO_NET_WORK_TYPE_EX_IS95A = 21 + MACRO_NET_WORK_TYPE_EX_IS95B = 22 + MACRO_NET_WORK_TYPE_EX_CDMA_1x = 23 + MACRO_NET_WORK_TYPE_EX_EVDO_REV_0 = 24 + MACRO_NET_WORK_TYPE_EX_EVDO_REV_A = 25 + MACRO_NET_WORK_TYPE_EX_EVDO_REV_B = 26 + MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x = 27 + MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0 = 28 + MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A = 29 + MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B = 30 + MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0 = 31 + MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A = 32 + MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B = 33 + MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0 = 34 + MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A = 35 + MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B = 36 + MACRO_NET_WORK_TYPE_EX_WCDMA = 41 + MACRO_NET_WORK_TYPE_EX_HSDPA = 42 + MACRO_NET_WORK_TYPE_EX_HSUPA = 43 + MACRO_NET_WORK_TYPE_EX_HSPA = 44 + MACRO_NET_WORK_TYPE_EX_HSPA_PLUS = 45 + MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS = 46 + MACRO_NET_WORK_TYPE_EX_TD_SCDMA = 61 + MACRO_NET_WORK_TYPE_EX_TD_HSDPA = 62 + MACRO_NET_WORK_TYPE_EX_TD_HSUPA = 63 + MACRO_NET_WORK_TYPE_EX_TD_HSPA = 64 + MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS = 65 + MACRO_NET_WORK_TYPE_EX_802_16E = 81 + MACRO_NET_WORK_TYPE_EX_LTE = 101 + + +def post_data_to_xml(data: dict, depth: int = 1) -> str: + if depth == 1: + return '<?xml version: "1.0" encoding="UTF-8"?>'+post_data_to_xml({'request': data}, depth+1) + + items = [] + for k, v in data.items(): + if isinstance(v, dict): + v = post_data_to_xml(v, depth+1) + elif isinstance(v, list): + raise TypeError('list type is unsupported here') + items.append(f'<{k}>{v}</{k}>') + + return ''.join(items) + + +class E3372: + _addr: Addr + _need_auth: bool + _legacy_token_auth: bool + _get_raw_data: bool + _headers: dict[str, str] + _authorized: bool + + def __init__(self, + addr: Addr, + need_auth: bool = True, + legacy_token_auth: bool = False, + get_raw_data: bool = False): + self._addr = addr + self._need_auth = need_auth + self._legacy_token_auth = legacy_token_auth + self._get_raw_data = get_raw_data + self._authorized = False + self._headers = {} + + @property + def device_information(self): + self.auth() + return self.request('device/information') + + @property + def device_signal(self): + self.auth() + return self.request('device/signal') + + @property + def monitoring_status(self): + self.auth() + return self.request('monitoring/status') + + @property + def notifications(self): + self.auth() + return self.request('monitoring/check-notifications') + + @property + def dialup_connection(self): + self.auth() + return self.request('dialup/connection') + + @property + def traffic_stats(self): + self.auth() + return self.request('monitoring/traffic-statistics') + + @property + def sms_count(self): + self.auth() + return self.request('sms/sms-count') + + def sms_send(self, phone: str, text: str): + self.auth() + return self.request('sms/send-sms', HTTPMethod.POST, { + 'Index': -1, + 'Phones': { + 'Phone': phone + }, + 'Sca': '', + 'Content': text, + 'Length': -1, + 'Reserved': 1, + 'Date': -1 + }) + + def sms_list(self, page: int = 1, count: int = 20, outbox: bool = False): + self.auth() + xml = self.request('sms/sms-list', HTTPMethod.POST, { + 'PageIndex': page, + 'ReadCount': count, + 'BoxType': 1 if not outbox else 2, + 'SortType': 0, + 'Ascending': 0, + 'UnreadPreferred': 1 if not outbox else 0 + }, return_body=True) + + root = ElementTree.fromstring(xml) + messages = [] + for message_elem in root.find('Messages').findall('Message'): + message_dict = {child.tag: child.text for child in message_elem} + messages.append(message_dict) + return messages + + def auth(self): + if self._authorized: + return + + if not self._legacy_token_auth: + data = self.request('webserver/SesTokInfo') + self._headers = { + 'Cookie': data['SesInfo'], + '__RequestVerificationToken': data['TokInfo'], + 'Content-Type': 'text/xml' + } + else: + data = self.request('webserver/token') + self._headers = { + '__RequestVerificationToken': data['token'], + 'Content-Type': 'text/xml' + } + + self._authorized = True + + def request(self, + method: str, + http_method: HTTPMethod = HTTPMethod.GET, + data: dict = {}, + return_body: bool = False) -> Union[str, dict]: + url = f'http://{self._addr}/api/{method}' + if http_method == HTTPMethod.POST: + data = post_data_to_xml(data) + f = requests.post + else: + data = None + f = requests.get + r = f(url, data=data, headers=self._headers) + r.raise_for_status() + r.encoding = 'utf-8' + + if return_body: + return r.text + + root = ElementTree.fromstring(r.text) + data_dict = {} + for elem in root: + data_dict[elem.tag] = elem.text + return data_dict diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py index 3c73440..f267488 100644 --- a/include/py/homekit/util.py +++ b/include/py/homekit/util.py @@ -12,7 +12,7 @@ import re import os from enum import Enum -from datetime import datetime +from datetime import datetime, timedelta from typing import Optional, List from zlib import adler32 @@ -255,6 +255,25 @@ def filesize_fmt(num, suffix="B") -> str: return f"{num:.1f} Yi{suffix}" +def seconds_to_human_readable_string(seconds: int) -> str: + duration = timedelta(seconds=seconds) + days, remainder = divmod(duration.total_seconds(), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + + parts = [] + if days > 0: + parts.append(f"{int(days)} day{'s' if days > 1 else ''}") + if hours > 0: + parts.append(f"{int(hours)} hour{'s' if hours > 1 else ''}") + if minutes > 0: + parts.append(f"{int(minutes)} minute{'s' if minutes > 1 else ''}") + if seconds > 0: + parts.append(f"{int(seconds)} second{'s' if seconds > 1 else ''}") + + return ' '.join(parts) + + class HashableEnum(Enum): def hash(self) -> int: return adler32(self.name.encode()) |