summaryrefslogtreecommitdiff
path: root/include
diff options
context:
space:
mode:
Diffstat (limited to 'include')
-rw-r--r--include/py/homekit/camera/config.py51
-rw-r--r--include/py/homekit/camera/types.py32
-rw-r--r--include/py/homekit/config/_configs.py1
-rw-r--r--include/py/homekit/config/config.py30
-rw-r--r--include/py/homekit/http/__init__.py4
-rw-r--r--include/py/homekit/http/http.py11
-rw-r--r--include/py/homekit/inverter/config.py4
-rw-r--r--include/py/homekit/modem/__init__.py2
-rw-r--r--include/py/homekit/modem/config.py29
-rw-r--r--include/py/homekit/modem/e3372.py253
-rw-r--r--include/py/homekit/mqtt/_config.py13
-rw-r--r--include/py/homekit/mqtt/module/temphum.py39
-rw-r--r--include/py/homekit/util.py67
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)
+ )