diff options
Diffstat (limited to 'include/py')
-rw-r--r-- | include/py/homekit/camera/config.py | 51 | ||||
-rw-r--r-- | include/py/homekit/camera/types.py | 32 | ||||
-rw-r--r-- | include/py/homekit/config/_configs.py | 1 | ||||
-rw-r--r-- | include/py/homekit/config/config.py | 30 | ||||
-rw-r--r-- | include/py/homekit/http/__init__.py | 4 | ||||
-rw-r--r-- | include/py/homekit/http/http.py | 11 | ||||
-rw-r--r-- | include/py/homekit/inverter/config.py | 4 | ||||
-rw-r--r-- | include/py/homekit/modem/__init__.py | 2 | ||||
-rw-r--r-- | include/py/homekit/modem/config.py | 29 | ||||
-rw-r--r-- | include/py/homekit/modem/e3372.py | 253 | ||||
-rw-r--r-- | include/py/homekit/mqtt/_config.py | 13 | ||||
-rw-r--r-- | include/py/homekit/mqtt/module/temphum.py | 39 | ||||
-rw-r--r-- | include/py/homekit/util.py | 67 |
13 files changed, 460 insertions, 76 deletions
diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py index c7dbc38..8aeb392 100644 --- a/include/py/homekit/camera/config.py +++ b/include/py/homekit/camera/config.py @@ -23,17 +23,13 @@ class IpcamConfig(ConfigUnit): @classmethod def schema(cls) -> Optional[dict]: return { - 'cams': { + 'cameras': { 'type': 'dict', 'keysrules': {'type': ['string', 'integer']}, 'valuesrules': { 'type': 'dict', 'schema': { 'type': {'type': 'string', 'allowed': [t.value for t in CameraType], 'required': True}, - 'codec': {'type': 'string', 'allowed': [t.value for t in VideoCodecType], 'required': True}, - 'container': {'type': 'string', 'allowed': [t.value for t in VideoContainerType], 'required': True}, - 'server': {'type': 'string', 'allowed': list(_lbc.get().keys()), 'required': True}, - 'disk': {'type': 'integer', 'required': True}, 'motion': { 'type': 'dict', 'schema': { @@ -44,10 +40,18 @@ class IpcamConfig(ConfigUnit): } } }, - 'rtsp_tcp': {'type': 'boolean'} } } }, + 'areas': { + 'type': 'dict', + 'keysrules': {'type': 'string'}, + 'valuesrules': { + 'type': 'list', + 'schema': {'type': ['string', 'integer']} # same type as for 'cameras' keysrules + } + }, + 'camera_ip_template': {'type': 'string', 'required': True}, 'motion_padding': {'type': 'integer', 'required': True}, 'motion_telegram': {'type': 'boolean', 'required': True}, 'fix_interval': {'type': 'integer', 'required': True}, @@ -69,6 +73,15 @@ class IpcamConfig(ConfigUnit): 'login': {'type': 'string', 'required': True}, 'password': {'type': 'string', 'required': True}, } + }, + + 'web_creds': { + 'required': True, + 'type': 'dict', + 'schema': { + 'login': {'type': 'string', 'required': True}, + 'password': {'type': 'string', 'required': True}, + } } } @@ -94,6 +107,7 @@ class IpcamConfig(ConfigUnit): } } + # FIXME def get_all_cam_names(self, filter_by_server: Optional[str] = None, filter_by_disk: Optional[int] = None) -> list[int]: @@ -106,25 +120,22 @@ class IpcamConfig(ConfigUnit): cams.append(int(cam)) return cams - def get_all_cam_names_for_this_server(self, - filter_by_disk: Optional[int] = None): - return self.get_all_cam_names(filter_by_server=socket.gethostname(), - filter_by_disk=filter_by_disk) + # def get_all_cam_names_for_this_server(self, + # filter_by_disk: Optional[int] = None): + # return self.get_all_cam_names(filter_by_server=socket.gethostname(), + # filter_by_disk=filter_by_disk) - def get_cam_server_and_disk(self, cam: int) -> tuple[str, int]: - return self['cams'][cam]['server'], self['cams'][cam]['disk'] + # def get_cam_server_and_disk(self, cam: int) -> tuple[str, int]: + # return self['cams'][cam]['server'], self['cams'][cam]['disk'] - def get_camera_container(self, cam: int) -> VideoContainerType: - return VideoContainerType(self['cams'][cam]['container']) + def get_camera_container(self, camera: int) -> VideoContainerType: + return self.get_camera_type(camera).get_container() - def get_camera_type(self, cam: int) -> CameraType: - return CameraType(self['cams'][cam]['type']) + def get_camera_type(self, camera: int) -> CameraType: + return CameraType(self['cams'][camera]['type']) def get_rtsp_creds(self) -> tuple[str, str]: return self['rtsp_creds']['login'], self['rtsp_creds']['password'] - def should_use_tcp_for_rtsp(self, cam: int) -> bool: - return 'rtsp_tcp' in self['cams'][cam] and self['cams'][cam]['rtsp_tcp'] - def get_camera_ip(self, camera: int) -> str: - return f'192.168.5.{camera}' + return self['camera_ip_template'] % (str(camera),) diff --git a/include/py/homekit/camera/types.py b/include/py/homekit/camera/types.py index c313b58..da0fcc6 100644 --- a/include/py/homekit/camera/types.py +++ b/include/py/homekit/camera/types.py @@ -1,10 +1,21 @@ from enum import Enum +class VideoContainerType(Enum): + MP4 = 'mp4' + MOV = 'mov' + + +class VideoCodecType(Enum): + H264 = 'h264' + H265 = 'h265' + + class CameraType(Enum): ESP32 = 'esp32' ALIEXPRESS_NONAME = 'ali' - HIKVISION = 'hik' + HIKVISION_264 = 'hik_264' + HIKVISION_265 = 'hik_265' def get_channel_url(self, channel: int) -> str: if channel not in (1, 2): @@ -12,22 +23,23 @@ class CameraType(Enum): if channel == 1: return '' elif channel == 2: - if self.value == CameraType.HIKVISION: + if self.value in (CameraType.HIKVISION_264, CameraType.HIKVISION_265): return '/Streaming/Channels/2' elif self.value == CameraType.ALIEXPRESS_NONAME: return '/?stream=1.sdp' else: raise ValueError(f'unsupported camera type {self.value}') + def get_codec(self, channel: int) -> VideoCodecType: + if channel == 1: + return VideoCodecType.H264 if self.value == CameraType.HIKVISION_264 else VideoCodecType.H265 + elif channel == 2: + return VideoCodecType.H265 if self.value == CameraType.ALIEXPRESS_NONAME else VideoCodecType.H264 + else: + raise ValueError(f'unexpected channel {channel}') -class VideoContainerType(Enum): - MP4 = 'mp4' - MOV = 'mov' - - -class VideoCodecType(Enum): - H264 = 'h264' - H265 = 'h265' + def get_container(self) -> VideoContainerType: + return VideoContainerType.MP4 if self.get_codec(1) == VideoCodecType.H264 else VideoContainerType.MOV class TimeFilterType(Enum): diff --git a/include/py/homekit/config/_configs.py b/include/py/homekit/config/_configs.py index f88c8ea..2cd2aca 100644 --- a/include/py/homekit/config/_configs.py +++ b/include/py/homekit/config/_configs.py @@ -26,6 +26,7 @@ class LinuxBoardsConfig(ConfigUnit): 'schema': { 'mdns': {'type': 'string', 'required': True}, 'board': {'type': 'string', 'required': True}, + 'location': {'type': 'string', 'required': True}, 'network': { 'type': 'list', 'required': True, diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index 5fe1ae8..fec92a6 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -41,6 +41,9 @@ class BaseConfigUnit(ABC): self._data = {} self._logger = logging.getLogger(self.__class__.__name__) + def __iter__(self): + return iter(self._data) + def __getitem__(self, key): return self._data[key] @@ -75,6 +78,15 @@ class BaseConfigUnit(ABC): raise KeyError(f'option {key} not found') + def values(self): + return self._data.values() + + def keys(self): + return self._data.keys() + + def items(self): + return self._data.items() + class ConfigUnit(BaseConfigUnit): NAME = 'dumb' @@ -123,10 +135,10 @@ class ConfigUnit(BaseConfigUnit): return None @classmethod - def _addr_schema(cls, required=False, **kwargs): + def _addr_schema(cls, required=False, only_ip=False, **kwargs): return { 'type': 'addr', - 'coerce': Addr.fromstring, + 'coerce': Addr.fromstring if not only_ip else Addr.fromipstring, 'required': required, **kwargs } @@ -158,6 +170,7 @@ class ConfigUnit(BaseConfigUnit): pass v = MyValidator() + need_document = False if rst == RootSchemaType.DICT: normalized = v.validated({'document': self._data}, @@ -165,16 +178,21 @@ class ConfigUnit(BaseConfigUnit): 'type': 'dict', 'keysrules': {'type': 'string'}, 'valuesrules': schema - }})['document'] + }}) + need_document = True elif rst == RootSchemaType.LIST: v = MyValidator() - normalized = v.validated({'document': self._data}, {'document': schema})['document'] + normalized = v.validated({'document': self._data}, {'document': schema}) + need_document = True else: normalized = v.validated(self._data, schema) if not normalized: raise cerberus.DocumentError(f'validation failed: {v.errors}') + if need_document: + normalized = normalized['document'] + self._data = normalized try: @@ -235,6 +253,8 @@ class TranslationUnit(BaseConfigUnit): class Translation: LANGUAGES = ('en', 'ru') + DEFAULT_LANGUAGE = 'ru' + _langs: dict[str, TranslationUnit] def __init__(self, name: str): @@ -278,9 +298,7 @@ class Config: and not isinstance(name, bool) \ and issubclass(name, AppConfigUnit) or name == AppConfigUnit: self.app_name = name.NAME - print(self.app_config) self.app_config = name() - print(self.app_config) app_config = self.app_config else: self.app_name = name if isinstance(name, str) else None 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..82c5aae 100644 --- a/include/py/homekit/http/http.py +++ b/include/py/homekit/http/http.py @@ -1,8 +1,9 @@ import logging import asyncio +from enum import Enum from aiohttp import web -from aiohttp.web import Response +from aiohttp.web import Response, HTTPFound from aiohttp.web_exceptions import HTTPNotFound from ..util import stringify, format_tb, Addr @@ -20,6 +21,9 @@ async def errors_handler_middleware(request, handler): except HTTPNotFound: return web.json_response({'error': 'not found'}, status=404) + except HTTPFound as exc: + raise exc + except Exception as exc: _logger.exception(exc) data = { @@ -104,3 +108,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/inverter/config.py b/include/py/homekit/inverter/config.py index e284dfe..694ddae 100644 --- a/include/py/homekit/inverter/config.py +++ b/include/py/homekit/inverter/config.py @@ -8,6 +8,6 @@ class InverterdConfig(ConfigUnit): @classmethod def schema(cls) -> Optional[dict]: return { - 'remote_addr': {'type': 'string'}, - 'local_addr': {'type': 'string'}, + 'remote_addr': cls._addr_schema(required=True), + 'local_addr': cls._addr_schema(required=True), }
\ No newline at end of file diff --git a/include/py/homekit/modem/__init__.py b/include/py/homekit/modem/__init__.py new file mode 100644 index 0000000..ea0930e --- /dev/null +++ b/include/py/homekit/modem/__init__.py @@ -0,0 +1,2 @@ +from .config import ModemsConfig +from .e3372 import E3372, MacroNetWorkType diff --git a/include/py/homekit/modem/config.py b/include/py/homekit/modem/config.py new file mode 100644 index 0000000..16d1ba0 --- /dev/null +++ b/include/py/homekit/modem/config.py @@ -0,0 +1,29 @@ +from ..config import ConfigUnit, Translation +from typing import Optional + + +class ModemsConfig(ConfigUnit): + NAME = 'modems' + + _strings: Translation + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._strings = Translation('modems') + + @classmethod + def schema(cls) -> Optional[dict]: + return { + 'type': 'dict', + 'schema': { + 'ip': cls._addr_schema(required=True, only_ip=True), + 'gateway_ip': cls._addr_schema(required=False, only_ip=True), + 'legacy_auth': {'type': 'boolean', 'required': True} + } + } + + def getshortname(self, modem: str, lang=Translation.DEFAULT_LANGUAGE): + return self._strings.get(lang)[modem]['short'] + + def getfullname(self, modem: str, lang=Translation.DEFAULT_LANGUAGE): + return self._strings.get(lang)[modem]['full']
\ No newline at end of file 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/mqtt/_config.py b/include/py/homekit/mqtt/_config.py index 4916d8a..8aa3bfe 100644 --- a/include/py/homekit/mqtt/_config.py +++ b/include/py/homekit/mqtt/_config.py @@ -92,6 +92,7 @@ class MqttNodesConfig(ConfigUnit): 'type': 'dict', 'schema': { 'module': {'type': 'string', 'required': True, 'allowed': ['si7021', 'dht12']}, + 'legacy_payload': {'type': 'boolean', 'required': False, 'default': False}, 'interval': {'type': 'integer'}, 'i2c_bus': {'type': 'integer'}, 'tcpserver': { @@ -168,3 +169,15 @@ class MqttNodesConfig(ConfigUnit): else: resdict[name] = node return reslist if only_names else resdict + + def node_uses_legacy_temphum_data_payload(self, node_id: str) -> bool: + try: + return self.get_node(node_id)['temphum']['legacy_payload'] + except KeyError: + return False + + def node_uses_legacy_relay_power_payload(self, node_id: str) -> bool: + try: + return self.get_node(node_id)['relay']['legacy_topics'] + except KeyError: + return False diff --git a/include/py/homekit/mqtt/module/temphum.py b/include/py/homekit/mqtt/module/temphum.py index fd02cca..6deccfe 100644 --- a/include/py/homekit/mqtt/module/temphum.py +++ b/include/py/homekit/mqtt/module/temphum.py @@ -10,8 +10,8 @@ MODULE_NAME = 'MqttTempHumModule' DATA_TOPIC = 'temphum/data' -class MqttTemphumDataPayload(MqttPayload): - FORMAT = '=ddb' +class MqttTemphumLegacyDataPayload(MqttPayload): + FORMAT = '=dd' UNPACKER = { 'temp': two_digits_precision, 'rh': two_digits_precision @@ -19,39 +19,26 @@ class MqttTemphumDataPayload(MqttPayload): temp: float rh: float - error: int -# class MqttTempHumNodes(HashableEnum): -# KBN_SH_HALL = auto() -# KBN_SH_BATHROOM = auto() -# KBN_SH_LIVINGROOM = auto() -# KBN_SH_BEDROOM = auto() -# -# KBN_BH_2FL = auto() -# KBN_BH_2FL_STREET = auto() -# KBN_BH_1FL_LIVINGROOM = auto() -# KBN_BH_1FL_BEDROOM = auto() -# KBN_BH_1FL_BATHROOM = auto() -# -# KBN_NH_1FL_INV = auto() -# KBN_NH_1FL_CENTER = auto() -# KBN_NH_1LF_KT = auto() -# KBN_NH_1FL_DS = auto() -# KBN_NH_1FS_EZ = auto() -# -# SPB_FLAT120_CABINET = auto() +class MqttTemphumDataPayload(MqttTemphumLegacyDataPayload): + FORMAT = '=ddb' + error: int class MqttTempHumModule(MqttModule): + _legacy_payload: bool + def __init__(self, sensor: Optional[BaseSensor] = None, + legacy_payload=False, write_to_database=False, *args, **kwargs): if sensor is not None: kwargs['tick_interval'] = 10 super().__init__(*args, **kwargs) self._sensor = sensor + self._legacy_payload = legacy_payload def on_connect(self, mqtt: MqttNode): super().on_connect(mqtt) @@ -69,7 +56,7 @@ class MqttTempHumModule(MqttModule): rh = self._sensor.humidity() except: error = 1 - pld = MqttTemphumDataPayload(temp=temp, rh=rh, error=error) + pld = self._get_data_payload_cls()(temp=temp, rh=rh, error=error) self._mqtt_node_ref.publish(DATA_TOPIC, pld.pack()) def handle_payload(self, @@ -77,6 +64,10 @@ class MqttTempHumModule(MqttModule): topic: str, payload: bytes) -> Optional[MqttPayload]: if topic == DATA_TOPIC: - message = MqttTemphumDataPayload.unpack(payload) + message = self._get_data_payload_cls().unpack(payload) self._logger.debug(message) return message + + def _get_data_payload_cls(self): + return MqttTemphumLegacyDataPayload if self._legacy_payload else MqttTemphumDataPayload + diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py index 22bba86..2b06600 100644 --- a/include/py/homekit/util.py +++ b/include/py/homekit/util.py @@ -9,6 +9,8 @@ import logging import string import random import re +import os +import ipaddress from enum import Enum from datetime import datetime @@ -36,6 +38,14 @@ def validate_ipv4_or_hostname(address: str, raise_exception: bool = False) -> bo return False +def validate_ipv4(address: str) -> bool: + try: + ipaddress.IPv6Address(address) + return True + except ipaddress.AddressValueError: + return False + + def validate_mac_address(mac_address: str) -> bool: mac_pattern = r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$' if re.match(mac_pattern, mac_address): @@ -52,17 +62,21 @@ class Addr: self.host = host self.port = port - @staticmethod - def fromstring(addr: str) -> Addr: - colons = addr.count(':') - if colons != 1: - raise ValueError('invalid host:port format') - - if not colons: - host = addr - port = None + @classmethod + def fromstring(cls, addr: str, port_required=True) -> Addr: + if port_required: + colons = addr.count(':') + if colons != 1: + raise ValueError('invalid host:port format') + + if not colons: + host = addr + port = None + else: + host, port = addr.split(':') else: - host, port = addr.split(':') + port = None + host = addr validate_ipv4_or_hostname(host, raise_exception=True) @@ -73,12 +87,19 @@ class Addr: return Addr(host, port) + @classmethod + def fromipstring(cls, addr: str) -> Addr: + return cls.fromstring(addr, port_required=False) + def __str__(self): buf = self.host if self.port is not None: buf += ':'+str(self.port) return buf + def __repr__(self): + return self.__str__() + def __iter__(self): yield self.host yield self.port @@ -243,6 +264,24 @@ def filesize_fmt(num, suffix="B") -> str: return f"{num:.1f} Yi{suffix}" +def seconds_to_human_readable_string(seconds: int) -> str: + days, remainder = divmod(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()) @@ -252,4 +291,10 @@ def next_tick_gen(freq): t = time.time() while True: t += freq - yield max(t - time.time(), 0)
\ No newline at end of file + yield max(t - time.time(), 0) + + +def homekit_path(*args) -> str: + return os.path.realpath( + os.path.join(os.path.dirname(__file__), '..', '..', '..', *args) + ) |