from datetime import datetime, timezone import struct import sys from typing import Any from ..const import ScreenLogicError from ..const.common import UNIT from ..const.msg import HEADER_FORMAT, HEADER_LENGTH from ..const.data import ATTR, DEVICE, GROUP, VALUE if sys.version_info[:2] < (3, 11): from async_timeout import timeout as asyncio_timeout # noqa F401 else: from asyncio import timeout as asyncio_timeout # noqa F401 def makeMessage(msgID: int, msgCode: int, messageData: bytes = b""): """Returns packed bytes formatted as a ready-to-send ScreenLogic message.""" return struct.pack( f"{HEADER_FORMAT}{str(len(messageData))}s", msgID, msgCode, len(messageData), messageData, ) def takeMessage(data: bytes) -> tuple[int, int, bytes]: """Return (messageID, messageCode, message) from raw ScreenLogic message bytes.""" messageBytes = len(data) - HEADER_LENGTH msgID, msgCode, msgLen, msgData = struct.unpack( f"{HEADER_FORMAT}{messageBytes}s", data ) if msgLen != messageBytes: raise ScreenLogicError( f"Response length invalid. Claimed: {msgLen}. Received: {messageBytes}. Message ID: {msgID}. Message Code: {msgCode}. Data: {data}" ) return msgID, msgCode, msgData # return raw data def takeMessages(data: bytes) -> list[tuple[int, int, bytes]]: messages = [] pos = 0 try: while pos < len(data): msgID, pos = getSome("H", data, pos) msgCode, pos = getSome("H", data, pos) msgLength, pos = getSome("I", data, pos) message, pos = getSome(f"{msgLength}s", data, pos) messages.append([msgID, msgCode, message]) return messages except struct.error as err: raise ScreenLogicError( f"Unexpected amount of data received. Data: {data}" ) from err def encodeMessageString(string: str, utf_16: bool = False) -> bytes: encoding = "utf-16" if utf_16 else "utf-8" data = string.encode(encoding) length = len(data) over = length % 4 pad = (4 - over) if over > 0 else 0 # pad string to multiple of 4 fmt = f" str: encoding = "utf-8" size = struct.unpack_from(" datetime: year, month, _, day, hour, minute, second, millisecond = struct.unpack("<8H", data) return datetime( year, month, day, hour, minute, second, millisecond * 1000, tzinfo=timezone.utc ) def getSome(format, buff, offset) -> tuple[Any, int]: fmt = format if format.startswith(">") else f"<{format}" newoffset = offset + struct.calcsize(fmt) return struct.unpack_from(fmt, buff, offset)[0], newoffset def getString(buff, offset) -> tuple[str, int]: fmtLen = " tuple[datetime, int]: fmt = "<8H" year, month, _, day, hour, minute, second, millisecond = struct.unpack_from( fmt, buff, offset ) new_offset = offset + struct.calcsize(fmt) return ( datetime( year, month, day, hour, minute, second, millisecond * 1000, timezone.utc ), new_offset, ) def getTemperatureUnit(data: dict): return ( UNIT.CELSIUS if data.get(DEVICE.CONTROLLER, {}) .get(GROUP.CONFIGURATION, {}) .get(VALUE.IS_CELSIUS, {}) .get(ATTR.VALUE) else UNIT.FAHRENHEIT ) def getAdapterVersion(data: dict): return data.get(DEVICE.ADAPTER, {}).get(VALUE.FIRMWARE, {}).get(ATTR.MINOR)