From e478fbb67cd1e6467ea890f61db50a2f4c796d05 Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Fri, 10 Dec 2021 14:25:43 +0100 Subject: [PATCH 01/10] Renamed lock_device as tasmota_device, and separated Codes Generator from lock_device --- custom_components/airbnk_mqtt/__init__.py | 4 +- .../airbnk_mqtt/codes_generator.py | 177 ++++++++++++++++++ .../{lock_device.py => tasmota_device.py} | 146 ++------------- 3 files changed, 190 insertions(+), 137 deletions(-) create mode 100644 custom_components/airbnk_mqtt/codes_generator.py rename custom_components/airbnk_mqtt/{lock_device.py => tasmota_device.py} (79%) diff --git a/custom_components/airbnk_mqtt/__init__.py b/custom_components/airbnk_mqtt/__init__.py index ebd3da6..62c5533 100644 --- a/custom_components/airbnk_mqtt/__init__.py +++ b/custom_components/airbnk_mqtt/__init__.py @@ -16,7 +16,7 @@ CONF_VOLTAGE_THRESHOLDS, CONF_USERID, ) -from .lock_device import AirbnkLockMqttDevice +from .tasmota_device import TasmotaMqttLockDevice _LOGGER = logging.getLogger(__name__) @@ -97,7 +97,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): _LOGGER.debug("DEVICES ARE %s", device_configs) lock_devices = {} for dev_id, dev_config in device_configs.items(): - lock_devices[dev_id] = AirbnkLockMqttDevice(hass, dev_config, entry.options) + lock_devices[dev_id] = TasmotaMqttLockDevice(hass, dev_config, entry.options) await lock_devices[dev_id].mqtt_subscribe() hass.data[DOMAIN] = {AIRBNK_DEVICES: lock_devices} diff --git a/custom_components/airbnk_mqtt/codes_generator.py b/custom_components/airbnk_mqtt/codes_generator.py new file mode 100644 index 0000000..0c75dc1 --- /dev/null +++ b/custom_components/airbnk_mqtt/codes_generator.py @@ -0,0 +1,177 @@ +from __future__ import annotations +import base64 +import binascii +import hashlib +import logging +import time + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +_LOGGER = logging.getLogger(__name__) + +MAX_NORECEIVE_TIME = 30 + + +class AESCipher: + """Cipher module for AES decryption.""" + + def __init__(self, key): + """Initialize a new AESCipher.""" + self.block_size = 16 + self.cipher = Cipher(algorithms.AES(key), modes.ECB(), default_backend()) + + def encrypt(self, raw, use_base64=True): + """Encrypt data to be sent to device.""" + encryptor = self.cipher.encryptor() + crypted_text = encryptor.update(self._pad(raw)) + encryptor.finalize() + return base64.b64encode(crypted_text) if use_base64 else crypted_text + + def decrypt(self, enc, use_base64=True): + """Decrypt data from device.""" + if use_base64: + enc = base64.b64decode(enc) + + decryptor = self.cipher.decryptor() + return self._unpad(decryptor.update(enc) + decryptor.finalize()) + + def _pad(self, data): + padnum = self.block_size - len(data) % self.block_size + return data + padnum * chr(padnum).encode() + + @staticmethod + def _unpad(data): + return data[: -ord(data[len(data) - 1 :])] + + +def XOR64Buffer(arr, value): + for i in range(0, 64): + arr[i] ^= value + return arr + + +def generateWorkingKey(arr, i): + arr2 = bytearray(72) + arr2[0 : len(arr)] = arr + arr2 = XOR64Buffer(arr2, 0x36) + arr2[71] = i & 0xFF + i = i >> 8 + arr2[70] = i & 0xFF + i = i >> 8 + arr2[69] = i & 0xFF + i = i >> 8 + arr2[68] = i & 0xFF + arr2sha1 = hashlib.sha1(arr2).digest() + arr3 = bytearray(84) + arr3[0 : len(arr)] = arr + arr3 = XOR64Buffer(arr3, 0x5C) + arr3[64:84] = arr2sha1 + arr3sha1 = hashlib.sha1(arr3).digest() + return arr3sha1 + + +def generatePswV2(arr): + arr2 = bytearray(8) + for i in range(0, 4): + b = arr[i + 16] + i2 = i * 2 + arr2[i2] = arr[(b >> 4) & 15] + arr2[i2 + 1] = arr[b & 15] + return arr2 + + +def generateSignatureV2(key, i, arr): + lenArr = len(arr) + arr2 = bytearray(lenArr + 68) + arr2[0:20] = key[0:20] + arr2 = XOR64Buffer(arr2, 0x36) + arr2[64 : 64 + lenArr] = arr + arr2[lenArr + 67] = i & 0xFF + i = i >> 8 + arr2[lenArr + 66] = i & 0xFF + i = i >> 8 + arr2[lenArr + 65] = i & 0xFF + i = i >> 8 + arr2[lenArr + 64] = i & 0xFF + arr2sha1 = hashlib.sha1(arr2).digest() + arr3 = bytearray(84) + arr3[0:20] = key[0:20] + arr3 = XOR64Buffer(arr3, 0x5C) + arr3[64 : 64 + len(arr2sha1)] = arr2sha1 + arr3sha1 = hashlib.sha1(arr3).digest() + return generatePswV2(arr3sha1) + + +def getCheckSum(arr, i1, i2): + c = 0 + for i in range(i1, i2): + c = c + arr[i] + return c & 0xFF + + +class AirbnkCodesGenerator: + manufactureKey = "" + bindingkey = "" + systemTime = 0 + + def __init__(self): + return + + def generateOperationCode(self, lock_dir, curr_lockEvents): + if lock_dir != 1 and lock_dir != 2: + return None + + self.systemTime = int(round(time.time())) + # self.systemTime = 1637590376 + opCode = self.makePackageV3(lock_dir, self.systemTime, curr_lockEvents) + _LOGGER.debug("OperationCode for dir %s is %s", lock_dir, opCode) + + return opCode + + def makePackageV3(self, lockOp, tStamp, curr_lockEvents): + code = bytearray(36) + code[0] = 0xAA + code[1] = 0x10 + code[2] = 0x1A + code[3] = code[4] = 3 + code[5] = 16 + lockOp + code[8] = 1 + code[12] = tStamp & 0xFF + tStamp = tStamp >> 8 + code[11] = tStamp & 0xFF + tStamp = tStamp >> 8 + code[10] = tStamp & 0xFF + tStamp = tStamp >> 8 + code[9] = tStamp & 0xFF + toEncrypt = code[4:18] + manKey = self.manufacturerKey[0:16] + encrypted = AESCipher(manKey).encrypt(toEncrypt, False) + code[4:20] = encrypted + workingKey = generateWorkingKey(self.bindingKey, 0) + signature = generateSignatureV2(workingKey, curr_lockEvents, code[3:20]) + # print("Working Key is {} {} {}".format(workingKey, lockEvents, code[3:20])) + # print("Signature is {}".format(signature)) + code[20 : 20 + len(signature)] = signature + code[20 + len(signature)] = getCheckSum(code, 3, 28) + return binascii.hexlify(code).upper() + # return code + + def decryptKeys(self, newSnInfo, appKey): + decr_json = {} + dec = base64.b64decode(newSnInfo) + sstr2 = dec[: len(dec) - 10] + key = appKey[: len(appKey) - 4] + dec = AESCipher(bytes(key, "utf-8")).decrypt(sstr2, False) + lockSn = dec[0:16].decode("utf-8").rstrip("\x00") + decr_json["lockSn"] = lockSn + decr_json["lockModel"] = dec[80:88].decode("utf-8").rstrip("\x00") + manKeyEncrypted = dec[16:48] + bindKeyEncrypted = dec[48:80] + toHash = bytes(lockSn + appKey, "utf-8") + hash_object = hashlib.sha1() + hash_object.update(toHash) + jdkSHA1 = hash_object.hexdigest() + key2 = bytes.fromhex(jdkSHA1[0:32]) + self.manufacturerKey = AESCipher(key2).decrypt(manKeyEncrypted, False) + self.bindingKey = AESCipher(key2).decrypt(bindKeyEncrypted, False) + return decr_json diff --git a/custom_components/airbnk_mqtt/lock_device.py b/custom_components/airbnk_mqtt/tasmota_device.py similarity index 79% rename from custom_components/airbnk_mqtt/lock_device.py rename to custom_components/airbnk_mqtt/tasmota_device.py index 9cb37d5..047fc20 100644 --- a/custom_components/airbnk_mqtt/lock_device.py +++ b/custom_components/airbnk_mqtt/tasmota_device.py @@ -1,7 +1,5 @@ from __future__ import annotations import base64 -import binascii -import hashlib import json import logging import time @@ -34,6 +32,8 @@ DEFAULT_RETRIES_NUM, ) +from .codes_generator import AirbnkCodesGenerator + _LOGGER = logging.getLogger(__name__) MAX_NORECEIVE_TIME = 30 @@ -79,7 +79,7 @@ def _unpad(data): return data[: -ord(data[len(data) - 1 :])] -class AirbnkLockMqttDevice: +class TasmotaMqttLockDevice: utcMinutes = None voltage = None @@ -97,17 +97,12 @@ class AirbnkLockMqttDevice: isMagnetEnable = None isBABA = None lversionOfSoft = None - versionOfSoft = None - versionCode = None + sversionOfSoft = None serialnumber = None lockEvents = 0 _lockConfig = {} _lockData = {} - lockModel = "" - lockSn = "" - manufactureKey = "" - bindingkey = "" - systemTime = 0 + _codes_generator = None frame1hex = "" frame2hex = "" frame1sent = False @@ -121,7 +116,8 @@ def __init__(self, hass: HomeAssistant, device_config, entry_options): self.hass = hass self._callbacks = set() self._lockConfig = device_config - self._lockData = self.decryptKeys( + self._codes_generator = AirbnkCodesGenerator() + self._lockData = self._codes_generator.decryptKeys( device_config["newSninfo"], device_config["appKey"] ) self.set_options(entry_options) @@ -302,96 +298,11 @@ async def operateLock(self, lock_dir): for callback_func in self._callbacks: callback_func() - self.generateOperationCode(lock_dir) - await self.async_sendFrame1() + opCode = self._codes_generator.generateOperationCode(lock_dir, self.lockEvents) + self.frame1hex = "FF00" + opCode[0:36].decode("utf-8") + self.frame2hex = "FF01" + opCode[36:].decode("utf-8") - def XOR64Buffer(self, arr, value): - for i in range(0, 64): - arr[i] ^= value - return arr - - def generateWorkingKey(self, arr, i): - arr2 = bytearray(72) - arr2[0 : len(arr)] = arr - arr2 = self.XOR64Buffer(arr2, 0x36) - arr2[71] = i & 0xFF - i = i >> 8 - arr2[70] = i & 0xFF - i = i >> 8 - arr2[69] = i & 0xFF - i = i >> 8 - arr2[68] = i & 0xFF - arr2sha1 = hashlib.sha1(arr2).digest() - arr3 = bytearray(84) - arr3[0 : len(arr)] = arr - arr3 = self.XOR64Buffer(arr3, 0x5C) - arr3[64:84] = arr2sha1 - arr3sha1 = hashlib.sha1(arr3).digest() - return arr3sha1 - - def generatePswV2(self, arr): - arr2 = bytearray(8) - for i in range(0, 4): - b = arr[i + 16] - i2 = i * 2 - arr2[i2] = arr[(b >> 4) & 15] - arr2[i2 + 1] = arr[b & 15] - return arr2 - - def generateSignatureV2(self, key, i, arr): - lenArr = len(arr) - arr2 = bytearray(lenArr + 68) - arr2[0:20] = key[0:20] - arr2 = self.XOR64Buffer(arr2, 0x36) - arr2[64 : 64 + lenArr] = arr - arr2[lenArr + 67] = i & 0xFF - i = i >> 8 - arr2[lenArr + 66] = i & 0xFF - i = i >> 8 - arr2[lenArr + 65] = i & 0xFF - i = i >> 8 - arr2[lenArr + 64] = i & 0xFF - arr2sha1 = hashlib.sha1(arr2).digest() - arr3 = bytearray(84) - arr3[0:20] = key[0:20] - arr3 = self.XOR64Buffer(arr3, 0x5C) - arr3[64 : 64 + len(arr2sha1)] = arr2sha1 - arr3sha1 = hashlib.sha1(arr3).digest() - return self.generatePswV2(arr3sha1) - - def getCheckSum(self, arr, i1, i2): - c = 0 - for i in range(i1, i2): - c = c + arr[i] - return c & 0xFF - - def makePackageV3(self, lockOp, tStamp): - code = bytearray(36) - code[0] = 0xAA - code[1] = 0x10 - code[2] = 0x1A - code[3] = code[4] = 3 - code[5] = 16 + lockOp - code[8] = 1 - code[12] = tStamp & 0xFF - tStamp = tStamp >> 8 - code[11] = tStamp & 0xFF - tStamp = tStamp >> 8 - code[10] = tStamp & 0xFF - tStamp = tStamp >> 8 - code[9] = tStamp & 0xFF - toEncrypt = code[4:18] - manKey = self._lockData["manufacturerKey"][0:16] - encrypted = AESCipher(manKey).encrypt(toEncrypt, False) - code[4:20] = encrypted - workingKey = self.generateWorkingKey(self._lockData["bindingKey"], 0) - signature = self.generateSignatureV2(workingKey, self.lockEvents, code[3:20]) - # print("Working Key is {} {} {}".format(workingKey, lockEvents, code[3:20])) - # print("Signature is {}".format(signature)) - code[20 : 20 + len(signature)] = signature - code[20 + len(signature)] = self.getCheckSum(code, 3, 28) - return binascii.hexlify(code).upper() - # return code + await self.async_sendFrame1() def requestDetails(self, mac_addr): mqtt.publish( @@ -615,38 +526,3 @@ def calculate_battery_percentage(self, voltage): perc = 33.3 + 33.3 * (voltage - voltages[0]) / (voltages[1] - voltages[0]) perc = max(perc, 0) return round(perc, 1) - - def generateOperationCode(self, lock_dir): - if lock_dir != 1 and lock_dir != 2: - return None - - self.systemTime = int(round(time.time())) - # self.systemTime = 1637590376 - opCode = self.makePackageV3(lock_dir, self.systemTime) - _LOGGER.debug("OperationCode for dir %s is %s", lock_dir, opCode) - self.frame1hex = "FF00" + opCode[0:36].decode("utf-8") - self.frame2hex = "FF01" + opCode[36:].decode("utf-8") - # print("PACKET 1 IS {}".format(self.frame1hex)) - # print("PACKET 2 IS {}".format(self.frame2hex)) - - return opCode - - def decryptKeys(self, newSnInfo, appKey): - json = {} - dec = base64.b64decode(newSnInfo) - sstr2 = dec[: len(dec) - 10] - key = appKey[: len(appKey) - 4] - dec = AESCipher(bytes(key, "utf-8")).decrypt(sstr2, False) - lockSn = dec[0:16].decode("utf-8").rstrip("\x00") - json["lockSn"] = lockSn - json["lockModel"] = dec[80:88].decode("utf-8").rstrip("\x00") - manKeyEncrypted = dec[16:48] - bindKeyEncrypted = dec[48:80] - toHash = bytes(lockSn + appKey, "utf-8") - hash_object = hashlib.sha1() - hash_object.update(toHash) - jdkSHA1 = hash_object.hexdigest() - key2 = bytes.fromhex(jdkSHA1[0:32]) - json["manufacturerKey"] = AESCipher(key2).decrypt(manKeyEncrypted, False) - json["bindingKey"] = AESCipher(key2).decrypt(bindKeyEncrypted, False) - return json From 029d9405138ed7d02c4d92f6b34307f34944b6cc Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Fri, 10 Dec 2021 16:45:13 +0100 Subject: [PATCH 02/10] Introduced selection between Custom and Tasmota MQTT in config flow --- custom_components/airbnk_mqtt/__init__.py | 13 +- custom_components/airbnk_mqtt/config_flow.py | 8 + custom_components/airbnk_mqtt/const.py | 5 + .../airbnk_mqtt/custom_device.py | 301 ++++++++++++++++++ custom_components/airbnk_mqtt/strings.json | 3 +- .../airbnk_mqtt/tasmota_device.py | 1 + .../airbnk_mqtt/translations/en.json | 3 +- 7 files changed, 330 insertions(+), 4 deletions(-) create mode 100644 custom_components/airbnk_mqtt/custom_device.py diff --git a/custom_components/airbnk_mqtt/__init__.py b/custom_components/airbnk_mqtt/__init__.py index 62c5533..bc6684c 100644 --- a/custom_components/airbnk_mqtt/__init__.py +++ b/custom_components/airbnk_mqtt/__init__.py @@ -15,8 +15,11 @@ CONF_DEVICE_CONFIGS, CONF_VOLTAGE_THRESHOLDS, CONF_USERID, + CONF_DEVICE_MQTT_TYPE, + CONF_CUSTOM_MQTT, ) from .tasmota_device import TasmotaMqttLockDevice +from .custom_device import CustomMqttLockDevice _LOGGER = logging.getLogger(__name__) @@ -97,8 +100,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): _LOGGER.debug("DEVICES ARE %s", device_configs) lock_devices = {} for dev_id, dev_config in device_configs.items(): - lock_devices[dev_id] = TasmotaMqttLockDevice(hass, dev_config, entry.options) - await lock_devices[dev_id].mqtt_subscribe() + if dev_config[CONF_DEVICE_MQTT_TYPE] == CONF_CUSTOM_MQTT: + lock_devices[dev_id] = CustomMqttLockDevice(hass, dev_config, entry.options) + else: + lock_devices[dev_id] = TasmotaMqttLockDevice( + hass, dev_config, entry.options + ) + await lock_devices[dev_id].mqtt_subscribe() + hass.data[DOMAIN] = {AIRBNK_DEVICES: lock_devices} for component in COMPONENT_TYPES: diff --git a/custom_components/airbnk_mqtt/config_flow.py b/custom_components/airbnk_mqtt/config_flow.py index 8e55903..57cb4ff 100644 --- a/custom_components/airbnk_mqtt/config_flow.py +++ b/custom_components/airbnk_mqtt/config_flow.py @@ -14,6 +14,8 @@ CONF_MAC_ADDRESS, CONF_DEVICE_CONFIGS, CONF_VOLTAGE_THRESHOLDS, + CONF_DEVICE_MQTT_TYPE, + CONF_MQTT_TYPES, CONF_RETRIES_NUM, DEFAULT_RETRIES_NUM, ) @@ -28,6 +30,9 @@ STEP3_SCHEMA = vol.Schema( { + vol.Required(CONF_DEVICE_MQTT_TYPE, default=CONF_MQTT_TYPES[0]): vol.In( + CONF_MQTT_TYPES + ), vol.Required(CONF_MAC_ADDRESS): str, vol.Required(CONF_MQTT_TOPIC): str, vol.Required(SKIP_DEVICE, default=False): bool, @@ -169,6 +174,9 @@ async def async_step_messagebox(self, user_input=None): dev_config = self.device_configs[config_key] action = "Skipped" if user_input.get(SKIP_DEVICE) is False: + dev_config[CONF_DEVICE_MQTT_TYPE] = user_input.get( + CONF_DEVICE_MQTT_TYPE + ) dev_config[CONF_MAC_ADDRESS] = ( user_input.get(CONF_MAC_ADDRESS).replace(":", "").upper() ) diff --git a/custom_components/airbnk_mqtt/const.py b/custom_components/airbnk_mqtt/const.py index 606292d..edb2d1c 100644 --- a/custom_components/airbnk_mqtt/const.py +++ b/custom_components/airbnk_mqtt/const.py @@ -27,6 +27,11 @@ CONF_VOLTAGE_THRESHOLDS = "voltage_thresholds" CONF_RETRIES_NUM = "retries_num" +CONF_DEVICE_MQTT_TYPE = "device_mqtt_type" +CONF_CUSTOM_MQTT = "Custom MQTT" +CONF_TASMOTA_MQTT = "Tasmota MQTT" +CONF_MQTT_TYPES = [CONF_CUSTOM_MQTT, CONF_TASMOTA_MQTT] + AIRBNK_DATA = "airbnk_data" AIRBNK_API = "airbnk_api" AIRBNK_DEVICES = "airbnk_devices" diff --git a/custom_components/airbnk_mqtt/custom_device.py b/custom_components/airbnk_mqtt/custom_device.py new file mode 100644 index 0000000..4ba62f5 --- /dev/null +++ b/custom_components/airbnk_mqtt/custom_device.py @@ -0,0 +1,301 @@ +from __future__ import annotations +import json +import logging +import time +from typing import Callable +from textwrap import wrap + +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.components import mqtt +from homeassistant.core import HomeAssistant, callback + +from .const import ( + DOMAIN as AIRBNK_DOMAIN, + SENSOR_TYPE_STATE, + SENSOR_TYPE_BATTERY, + SENSOR_TYPE_VOLTAGE, + SENSOR_TYPE_LAST_ADVERT, + SENSOR_TYPE_SIGNAL_STRENGTH, + LOCK_STATE_LOCKED, + LOCK_STATE_UNLOCKED, + LOCK_STATE_JAMMED, + LOCK_STATE_OPERATING, + LOCK_STATE_FAILED, + LOCK_STATE_STRINGS, + CONF_MAC_ADDRESS, + CONF_MQTT_TOPIC, + CONF_VOLTAGE_THRESHOLDS, + CONF_RETRIES_NUM, + DEFAULT_RETRIES_NUM, +) + +_LOGGER = logging.getLogger(__name__) + +MAX_NORECEIVE_TIME = 30 + +BLETelemetryTopic = "%s/tele" +BLEOpTopic = "%s/command" +BLEStateTopic = "%s/adv" +BLEOperationReportTopic = "%s/command_result" + + +class CustomMqttLockDevice: + + utcMinutes = None + voltage = None + isBackLock = None + isInit = None + isImageA = None + isHadNewRecord = None + curr_state = LOCK_STATE_UNLOCKED + softVersion = None + isEnableAuto = None + opensClockwise = None + isLowBattery = None + magnetcurr_state = None + isMagnetEnable = None + isBABA = None + lversionOfSoft = None + versionOfSoft = None + versionCode = None + serialnumber = None + lockEvents = 0 + _lockConfig = {} + _lockData = {} + cmd = {} + cmdSent = False + last_advert_time = 0 + is_available = False + + def __init__(self, hass: HomeAssistant, device_config, entry_options): + _LOGGER.debug("Setting up CustomMqttLockDevice for sn %s", device_config["sn"]) + self.hass = hass + self._callbacks = set() + self._lockConfig = device_config + mac_addr = self._lockConfig[CONF_MAC_ADDRESS] + if ":" not in mac_addr: + self._lockConfig[CONF_MAC_ADDRESS] = ":".join(wrap(mac_addr, 2)) + self._lockData = self.decryptKeys( + device_config["newSninfo"], device_config["appKey"] + ) + self.set_options(entry_options) + + @property + def device_info(self): + """Return a device description for device registry.""" + devID = self._lockData["lockSn"] + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (AIRBNK_DOMAIN, devID) + }, + "manufacturer": "Airbnk", + "model": self._lockConfig["deviceType"], + "name": self._lockConfig["deviceName"], + "sw_version": self._lockConfig["firmwareVersion"], + "connections": { + (CONNECTION_NETWORK_MAC, self._lockConfig[CONF_MAC_ADDRESS]) + }, + } + + def check_availability(self): + curr_time = int(round(time.time())) + deltatime = curr_time - self.last_advert_time + # _LOGGER.debug("Last reply was %s secs ago", deltatime) + if deltatime >= MAX_NORECEIVE_TIME: + self.is_available = False + + @property + def islocked(self) -> bool | None: + if self.curr_state == LOCK_STATE_LOCKED: + return True + else: + return False + + @property + def isunlocked(self) -> bool | None: + if self.curr_state == LOCK_STATE_UNLOCKED: + return True + else: + return False + + @property + def isjammed(self) -> bool | None: + if self.curr_state == LOCK_STATE_JAMMED: + return True + else: + return False + + @property + def state(self): + return LOCK_STATE_STRINGS[self.curr_state] + + def set_options(self, entry_options): + """Register callback, called when lock changes state.""" + _LOGGER.debug("Options set: %s", entry_options) + self.retries_num = entry_options.get(CONF_RETRIES_NUM, DEFAULT_RETRIES_NUM) + + async def mqtt_subscribe(self): + @callback + async def adv_received(_p0) -> None: + self.parse_adv_message(_p0.payload) + + @callback + async def operation_msg_received(_p0) -> None: + self.parse_operation_message(_p0.payload) + + @callback + async def telemetry_msg_received(_p0) -> None: + self.parse_telemetry_message(_p0.payload) + + await mqtt.async_subscribe( + self.hass, + BLEStateTopic % self._lockConfig[CONF_MQTT_TOPIC], + msg_callback=adv_received, + ) + await mqtt.async_subscribe( + self.hass, + BLETelemetryTopic % self._lockConfig[CONF_MQTT_TOPIC], + msg_callback=telemetry_msg_received, + ) + await mqtt.async_subscribe( + self.hass, + BLEOperationReportTopic % self._lockConfig[CONF_MQTT_TOPIC], + msg_callback=operation_msg_received, + ) + + def register_callback(self, callback: Callable[[], None]) -> None: + """Register callback, called when lock changes state.""" + self._callbacks.add(callback) + + def parse_telemetry_message(self, msg): + # TODO + _LOGGER.debug("Received telemetry %s", msg) + + def parse_adv_message(self, msg): + _LOGGER.debug("Received adv %s", msg) + payload = json.loads(msg) + mac_address = self._lockConfig[CONF_MAC_ADDRESS] + mqtt_advert = payload["data"] + mqtt_mac = payload["mac"].replace(":", "").upper() + _LOGGER.debug("Config mac %s, received %s", mac_address, mqtt_mac) + if mqtt_mac != mac_address.upper(): + return + + self.parse_MQTT_advert(mqtt_advert.upper()) + time2 = self.last_advert_time + self.last_advert_time = int(round(time.time())) + if "rssi" in payload: + rssi = payload["rssi"] + self._lockData[SENSOR_TYPE_SIGNAL_STRENGTH] = rssi + + deltatime = self.last_advert_time - time2 + self._lockData[SENSOR_TYPE_LAST_ADVERT] = deltatime + self.is_available = True + _LOGGER.debug("Time from last message: %s secs", str(deltatime)) + + for callback_func in self._callbacks: + callback_func() + + def parse_operation_message(self, msg): + _LOGGER.debug("Received operation result %s", msg) + payload = json.loads(msg) + mac_address = self._lockConfig[CONF_MAC_ADDRESS] + mqtt_mac = payload["mac"].replace(":", "").upper() + + if mqtt_mac != mac_address.upper(): + return + + msg_state = payload["success"] + if msg_state is False: + _LOGGER.error("Failed sending command: returned %s", msg_state) + self.curr_state = LOCK_STATE_FAILED + raise Exception("Failed sending command: returned %s", msg_state) + return + + msg_sign = payload["sign"] + if msg_sign == self.cmd["sign"]: + self.cmdSent = True + + for callback_func in self._callbacks: + callback_func() + + async def operateLock(self, lock_dir): + _LOGGER.debug("operateLock called (%s)", lock_dir) + self.cmdSent = False + self.curr_state = LOCK_STATE_OPERATING + for callback_func in self._callbacks: + callback_func() + + opCode = self._codes_generator.generateOperationCode(lock_dir, self.lockEvents) + self.cmd = {} + self.cmd["command1"] = "FF00" + opCode[0:36].decode("utf-8") + self.cmd["command2"] = "FF01" + opCode[36:].decode("utf-8") + self.cmd["sign"] = self._codes_generator.systemTime + mqtt.publish( + self.hass, + BLEOpTopic % self._lockConfig[CONF_MQTT_TOPIC], + json.dumps(self.cmd), + ) + + def parse_MQTT_advert(self, mqtt_advert): + _LOGGER.debug("Parsing advert msg: %s", mqtt_advert) + bArr = bytearray.fromhex(mqtt_advert) + if bArr[0] != 0xBA or bArr[1] != 0xBA: + _LOGGER.error("Wrong advert msg: %s", mqtt_advert) + return + + self.voltage = ((float)((bArr[16] << 8) | bArr[17])) * 0.1 + self.boardModel = bArr[2] + self.lversionOfSoft = bArr[3] + self.sversionOfSoft = (bArr[4] << 16) | (bArr[5] << 8) | bArr[6] + serialnumber = bArr[7:16].decode("utf-8").strip("\0") + if serialnumber != self._lockConfig["sn"]: + _LOGGER.error( + "ERROR: s/n in advert (%s) is different from cloud data (%s)", + serialnumber, + self._lockConfig["sn"], + ) + + lockEvents = (bArr[18] << 24) | (bArr[19] << 16) | (bArr[20] << 8) | bArr[21] + new_state = (bArr[22] >> 4) & 3 + self.opensClockwise = (bArr[22] & 0x80) != 0 + if self.curr_state < LOCK_STATE_OPERATING or self.lockEvents != lockEvents: + self.lockEvents = lockEvents + self.curr_state = new_state + if self.opensClockwise and self.curr_state is not LOCK_STATE_JAMMED: + self.curr_state = 1 - self.curr_state + + z = False + self.isBackLock = (bArr[22] & 1) != 0 + self.isInit = (2 & bArr[22]) != 0 + self.isImageA = (bArr[22] & 4) != 0 + self.isHadNewRecord = (bArr[22] & 8) != 0 + self.isEnableAuto = (bArr[22] & 0x40) != 0 + self.isLowBattery = (bArr[23] & 0x10) != 0 + self.magnetcurr_state = (bArr[23] >> 5) & 3 + if (bArr[23] & 0x80) != 0: + z = True + + self.isMagnetEnable = z + self.isBABA = True + + self.battery_perc = self.calculate_battery_percentage(self.voltage) + self._lockData[SENSOR_TYPE_STATE] = self.state + self._lockData[SENSOR_TYPE_BATTERY] = self.battery_perc + self._lockData[SENSOR_TYPE_VOLTAGE] = self.voltage + # print("LOCK: {}".format(self._lockData)) + + return + + def calculate_battery_percentage(self, voltage): + voltages = self._lockConfig[CONF_VOLTAGE_THRESHOLDS] + perc = 0 + if voltage >= voltages[2]: + perc = 100 + elif voltage >= voltages[1]: + perc = 66.6 + 33.3 * (voltage - voltages[1]) / (voltages[2] - voltages[1]) + else: + perc = 33.3 + 33.3 * (voltage - voltages[0]) / (voltages[1] - voltages[0]) + perc = max(perc, 0) + return round(perc, 1) diff --git a/custom_components/airbnk_mqtt/strings.json b/custom_components/airbnk_mqtt/strings.json index 3097356..b003648 100644 --- a/custom_components/airbnk_mqtt/strings.json +++ b/custom_components/airbnk_mqtt/strings.json @@ -18,8 +18,9 @@ }, "configure_device": { "title": "Enter device parameters", - "description": "Enter the MAC address and topic for MQTT connection with {model} lock, s/n {sn}.", + "description": "Enter the device type, MAC address and topic\nfor MQTT connection with {model} lock, s/n {sn}.", "data": { + "device_mqtt_type": "[%key:common::config_flow::data::device_mqtt_type]", "mac_address": "[%key:common::config_flow::data::mac_address]", "mqtt_topic": "[%key:common::config_flow::data::mqtt_topic]", "skip_device": "[%key:common::config_flow::data::skip_device]" diff --git a/custom_components/airbnk_mqtt/tasmota_device.py b/custom_components/airbnk_mqtt/tasmota_device.py index 047fc20..7406518 100644 --- a/custom_components/airbnk_mqtt/tasmota_device.py +++ b/custom_components/airbnk_mqtt/tasmota_device.py @@ -113,6 +113,7 @@ class TasmotaMqttLockDevice: curr_try = 0 def __init__(self, hass: HomeAssistant, device_config, entry_options): + _LOGGER.debug("Setting up TasmotaMqttLockDevice for sn %s", device_config["sn"]) self.hass = hass self._callbacks = set() self._lockConfig = device_config diff --git a/custom_components/airbnk_mqtt/translations/en.json b/custom_components/airbnk_mqtt/translations/en.json index d93ae34..0ae8294 100644 --- a/custom_components/airbnk_mqtt/translations/en.json +++ b/custom_components/airbnk_mqtt/translations/en.json @@ -33,8 +33,9 @@ }, "configure_device": { "title": "Enter device parameters", - "description": "Enter the MAC address and topic for MQTT connection with {model} lock, s/n {sn}.", + "description": "Enter the device type, MAC address and topic\nfor MQTT connection with {model} lock, s/n {sn}.", "data": { + "device_mqtt_type": "Device MQTT type", "mac_address": "MAC address", "mqtt_topic": "MQTT topic", "skip_device": "Skip this device" From 2d3f8288d59c10329cbbf8dbc9c97023b91afe57 Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Fri, 10 Dec 2021 17:07:27 +0100 Subject: [PATCH 03/10] Fixed missing code generator in Custom device --- custom_components/airbnk_mqtt/custom_device.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_components/airbnk_mqtt/custom_device.py b/custom_components/airbnk_mqtt/custom_device.py index 4ba62f5..617df48 100644 --- a/custom_components/airbnk_mqtt/custom_device.py +++ b/custom_components/airbnk_mqtt/custom_device.py @@ -29,6 +29,8 @@ DEFAULT_RETRIES_NUM, ) +from .codes_generator import AirbnkCodesGenerator + _LOGGER = logging.getLogger(__name__) MAX_NORECEIVE_TIME = 30 @@ -62,6 +64,7 @@ class CustomMqttLockDevice: lockEvents = 0 _lockConfig = {} _lockData = {} + _codes_generator = None cmd = {} cmdSent = False last_advert_time = 0 @@ -72,10 +75,11 @@ def __init__(self, hass: HomeAssistant, device_config, entry_options): self.hass = hass self._callbacks = set() self._lockConfig = device_config + self._codes_generator = AirbnkCodesGenerator() mac_addr = self._lockConfig[CONF_MAC_ADDRESS] if ":" not in mac_addr: self._lockConfig[CONF_MAC_ADDRESS] = ":".join(wrap(mac_addr, 2)) - self._lockData = self.decryptKeys( + self._lockData = self._codes_generator.decryptKeys( device_config["newSninfo"], device_config["appKey"] ) self.set_options(entry_options) From 8e32e0604f4554867c0cc49150ce2b32faa905cc Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Fri, 10 Dec 2021 17:24:03 +0100 Subject: [PATCH 04/10] More fixes for Custom device --- custom_components/airbnk_mqtt/__init__.py | 6 ++++-- custom_components/airbnk_mqtt/custom_device.py | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/airbnk_mqtt/__init__.py b/custom_components/airbnk_mqtt/__init__.py index bc6684c..79eab22 100644 --- a/custom_components/airbnk_mqtt/__init__.py +++ b/custom_components/airbnk_mqtt/__init__.py @@ -101,12 +101,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): lock_devices = {} for dev_id, dev_config in device_configs.items(): if dev_config[CONF_DEVICE_MQTT_TYPE] == CONF_CUSTOM_MQTT: - lock_devices[dev_id] = CustomMqttLockDevice(hass, dev_config, entry.options) + lock_devices[dev_id] = CustomMqttLockDevice( + hass, dev_config, entry.options + ) else: lock_devices[dev_id] = TasmotaMqttLockDevice( hass, dev_config, entry.options ) - await lock_devices[dev_id].mqtt_subscribe() + await lock_devices[dev_id].mqtt_subscribe() hass.data[DOMAIN] = {AIRBNK_DEVICES: lock_devices} diff --git a/custom_components/airbnk_mqtt/custom_device.py b/custom_components/airbnk_mqtt/custom_device.py index 617df48..1fb711d 100644 --- a/custom_components/airbnk_mqtt/custom_device.py +++ b/custom_components/airbnk_mqtt/custom_device.py @@ -77,8 +77,6 @@ def __init__(self, hass: HomeAssistant, device_config, entry_options): self._lockConfig = device_config self._codes_generator = AirbnkCodesGenerator() mac_addr = self._lockConfig[CONF_MAC_ADDRESS] - if ":" not in mac_addr: - self._lockConfig[CONF_MAC_ADDRESS] = ":".join(wrap(mac_addr, 2)) self._lockData = self._codes_generator.decryptKeys( device_config["newSninfo"], device_config["appKey"] ) From 2da6f922d8c567918f1d2b6d7a734d9d084e7a71 Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Fri, 10 Dec 2021 18:04:00 +0100 Subject: [PATCH 05/10] Fixed voltage in Custom device --- custom_components/airbnk_mqtt/custom_device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/airbnk_mqtt/custom_device.py b/custom_components/airbnk_mqtt/custom_device.py index 1fb711d..6646396 100644 --- a/custom_components/airbnk_mqtt/custom_device.py +++ b/custom_components/airbnk_mqtt/custom_device.py @@ -247,7 +247,7 @@ def parse_MQTT_advert(self, mqtt_advert): _LOGGER.error("Wrong advert msg: %s", mqtt_advert) return - self.voltage = ((float)((bArr[16] << 8) | bArr[17])) * 0.1 + self.voltage = ((float)((bArr[16] << 8) | bArr[17])) * 0.01 self.boardModel = bArr[2] self.lversionOfSoft = bArr[3] self.sversionOfSoft = (bArr[4] << 16) | (bArr[5] << 8) | bArr[6] From 293c208b3e93493202de08f32dbb384e5cbc06cf Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Mon, 13 Dec 2021 10:54:19 +0100 Subject: [PATCH 06/10] Improved non-lock devices filtering --- custom_components/airbnk_mqtt/airbnk_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/airbnk_mqtt/airbnk_api.py b/custom_components/airbnk_mqtt/airbnk_api.py index 774ab90..9c7f0cc 100644 --- a/custom_components/airbnk_mqtt/airbnk_api.py +++ b/custom_components/airbnk_mqtt/airbnk_api.py @@ -119,7 +119,7 @@ async def getCloudDevices(hass, userId, token): res = {} deviceConfigs = {} for dev_data in json_data["data"] or []: - if dev_data["gateway"] == "": + if dev_data["boardModel"].isnumeric(): _LOGGER.info("Device '%s' is filtered out", dev_data["deviceName"]) else: res[dev_data["sn"]] = dev_data["deviceName"] From 74c5aadc387f2f859910c6a6061c3ac1d1df1205 Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Mon, 13 Dec 2021 11:29:28 +0100 Subject: [PATCH 07/10] Fixed non-lock devices filtering --- custom_components/airbnk_mqtt/airbnk_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/airbnk_mqtt/airbnk_api.py b/custom_components/airbnk_mqtt/airbnk_api.py index 9c7f0cc..e64cba0 100644 --- a/custom_components/airbnk_mqtt/airbnk_api.py +++ b/custom_components/airbnk_mqtt/airbnk_api.py @@ -119,7 +119,7 @@ async def getCloudDevices(hass, userId, token): res = {} deviceConfigs = {} for dev_data in json_data["data"] or []: - if dev_data["boardModel"].isnumeric(): + if not dev_data["boardModel"].isnumeric(): _LOGGER.info("Device '%s' is filtered out", dev_data["deviceName"]) else: res[dev_data["sn"]] = dev_data["deviceName"] From 0481658300031737a406cc8db4a3da592e82231e Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Mon, 13 Dec 2021 16:07:37 +0100 Subject: [PATCH 08/10] Introduced auto retries in custom_device.py --- custom_components/airbnk_mqtt/__init__.py | 4 +-- .../airbnk_mqtt/custom_device.py | 35 ++++++++++++++----- .../airbnk_mqtt/tasmota_device.py | 10 +++--- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/custom_components/airbnk_mqtt/__init__.py b/custom_components/airbnk_mqtt/__init__.py index 79eab22..f4aade9 100644 --- a/custom_components/airbnk_mqtt/__init__.py +++ b/custom_components/airbnk_mqtt/__init__.py @@ -101,9 +101,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): lock_devices = {} for dev_id, dev_config in device_configs.items(): if dev_config[CONF_DEVICE_MQTT_TYPE] == CONF_CUSTOM_MQTT: - lock_devices[dev_id] = CustomMqttLockDevice( - hass, dev_config, entry.options - ) + lock_devices[dev_id] = CustomMqttLockDevice(hass, dev_config, entry.options) else: lock_devices[dev_id] = TasmotaMqttLockDevice( hass, dev_config, entry.options diff --git a/custom_components/airbnk_mqtt/custom_device.py b/custom_components/airbnk_mqtt/custom_device.py index 6646396..76d4e8c 100644 --- a/custom_components/airbnk_mqtt/custom_device.py +++ b/custom_components/airbnk_mqtt/custom_device.py @@ -3,7 +3,6 @@ import logging import time from typing import Callable -from textwrap import wrap from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.components import mqtt @@ -68,7 +67,10 @@ class CustomMqttLockDevice: cmd = {} cmdSent = False last_advert_time = 0 + last_telemetry_time = 0 is_available = False + retries_num = DEFAULT_RETRIES_NUM + curr_try = 0 def __init__(self, hass: HomeAssistant, device_config, entry_options): _LOGGER.debug("Setting up CustomMqttLockDevice for sn %s", device_config["sn"]) @@ -76,7 +78,6 @@ def __init__(self, hass: HomeAssistant, device_config, entry_options): self._callbacks = set() self._lockConfig = device_config self._codes_generator = AirbnkCodesGenerator() - mac_addr = self._lockConfig[CONF_MAC_ADDRESS] self._lockData = self._codes_generator.decryptKeys( device_config["newSninfo"], device_config["appKey"] ) @@ -102,9 +103,10 @@ def device_info(self): def check_availability(self): curr_time = int(round(time.time())) - deltatime = curr_time - self.last_advert_time - # _LOGGER.debug("Last reply was %s secs ago", deltatime) - if deltatime >= MAX_NORECEIVE_TIME: + deltatime1 = curr_time - self.last_advert_time + deltatime2 = curr_time - self.last_telemetry_time + # _LOGGER.debug("Last reply was %s - %s secs ago", deltatime1, deltatime2) + if min(deltatime1, deltatime2) >= MAX_NORECEIVE_TIME: self.is_available = False @property @@ -173,6 +175,8 @@ def register_callback(self, callback: Callable[[], None]) -> None: def parse_telemetry_message(self, msg): # TODO _LOGGER.debug("Received telemetry %s", msg) + self.last_telemetry_time = int(round(time.time())) + self.is_available = True def parse_adv_message(self, msg): _LOGGER.debug("Received adv %s", msg) @@ -210,9 +214,20 @@ def parse_operation_message(self, msg): msg_state = payload["success"] if msg_state is False: - _LOGGER.error("Failed sending command: returned %s", msg_state) - self.curr_state = LOCK_STATE_FAILED - raise Exception("Failed sending command: returned %s", msg_state) + if self.curr_try < self.retries_num: + self.curr_try += 1 + time.sleep(0.5) + _LOGGER.debug("Retrying: attempt %i", self.curr_try) + self.curr_state = LOCK_STATE_OPERATING + for callback_func in self._callbacks: + callback_func() + self.send_mqtt_command() + else: + _LOGGER.error("No more retries: command FAILED") + self.curr_state = LOCK_STATE_FAILED + for callback_func in self._callbacks: + callback_func() + raise Exception("Failed sending command: returned %s", msg_state) return msg_sign = payload["sign"] @@ -224,6 +239,7 @@ def parse_operation_message(self, msg): async def operateLock(self, lock_dir): _LOGGER.debug("operateLock called (%s)", lock_dir) + self.curr_try = 0 self.cmdSent = False self.curr_state = LOCK_STATE_OPERATING for callback_func in self._callbacks: @@ -234,6 +250,9 @@ async def operateLock(self, lock_dir): self.cmd["command1"] = "FF00" + opCode[0:36].decode("utf-8") self.cmd["command2"] = "FF01" + opCode[36:].decode("utf-8") self.cmd["sign"] = self._codes_generator.systemTime + self.send_mqtt_command() + + def send_mqtt_command(self): mqtt.publish( self.hass, BLEOpTopic % self._lockConfig[CONF_MQTT_TOPIC], diff --git a/custom_components/airbnk_mqtt/tasmota_device.py b/custom_components/airbnk_mqtt/tasmota_device.py index 7406518..b773743 100644 --- a/custom_components/airbnk_mqtt/tasmota_device.py +++ b/custom_components/airbnk_mqtt/tasmota_device.py @@ -259,9 +259,6 @@ async def async_parse_MQTT_message(self, msg): msg_state = payload[msg_type]["state"] if "FAIL" in msg_state: _LOGGER.error("Failed sending frame: returned %s", msg_state) - self.curr_state = LOCK_STATE_FAILED - for callback_func in self._callbacks: - callback_func() if self.curr_try < self.retries_num: self.curr_try += 1 @@ -276,6 +273,9 @@ async def async_parse_MQTT_message(self, msg): await self.async_sendFrame1() else: _LOGGER.error("No more retries: command FAILED") + self.curr_state = LOCK_STATE_FAILED + for callback_func in self._callbacks: + callback_func() raise Exception("Failed sending frame: returned %s", msg_state) return @@ -322,14 +322,14 @@ def scanAllAdverts(self): ) async def async_sendFrame1(self): - mqtt.publish( + await mqtt.async_publish( self.hass, BLEOpTopic % self._lockConfig[CONF_MQTT_TOPIC], self.BLEOPWritePAYLOADGen(self.frame1hex), ) async def async_sendFrame2(self): - mqtt.publish( + await mqtt.async_publish( self.hass, BLEOpTopic % self._lockConfig[CONF_MQTT_TOPIC], self.BLEOPWritePAYLOADGen(self.frame2hex), From 540f7878b7e8ccc1f81ebe699acc7221c1408d5f Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Wed, 15 Dec 2021 22:04:45 +0100 Subject: [PATCH 09/10] Fixed non-lock devices filtering --- custom_components/airbnk_mqtt/airbnk_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/airbnk_mqtt/airbnk_api.py b/custom_components/airbnk_mqtt/airbnk_api.py index e64cba0..611b31d 100644 --- a/custom_components/airbnk_mqtt/airbnk_api.py +++ b/custom_components/airbnk_mqtt/airbnk_api.py @@ -119,7 +119,7 @@ async def getCloudDevices(hass, userId, token): res = {} deviceConfigs = {} for dev_data in json_data["data"] or []: - if not dev_data["boardModel"].isnumeric(): + if dev_data["deviceType"][0] in ["W", "F"]: _LOGGER.info("Device '%s' is filtered out", dev_data["deviceName"]) else: res[dev_data["sn"]] = dev_data["deviceName"] From 6b0a9bed1f812198c17aee33f0467447559e3c91 Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Wed, 15 Dec 2021 22:07:00 +0100 Subject: [PATCH 10/10] Introduced parsing of lockStatus field (i.e. FFF3 characteristic) --- custom_components/airbnk_mqtt/custom_device.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/custom_components/airbnk_mqtt/custom_device.py b/custom_components/airbnk_mqtt/custom_device.py index 76d4e8c..bf65088 100644 --- a/custom_components/airbnk_mqtt/custom_device.py +++ b/custom_components/airbnk_mqtt/custom_device.py @@ -234,6 +234,8 @@ def parse_operation_message(self, msg): if msg_sign == self.cmd["sign"]: self.cmdSent = True + self.parse_new_lockStatus(payload["lockStatus"]) + for callback_func in self._callbacks: callback_func() @@ -259,6 +261,18 @@ def send_mqtt_command(self): json.dumps(self.cmd), ) + def parse_new_lockStatus(self, lockStatus): + _LOGGER.debug("Parsing new lockStatus: %s", lockStatus) + bArr = bytearray.fromhex(lockStatus) + if bArr[0] != 0xAA or bArr[3] != 0x02 or bArr[4] != 0x04: + _LOGGER.error("Wrong lockStatus msg: %s", lockStatus) + return + + lockEvents = (bArr[10] << 24) | (bArr[11] << 16) | (bArr[12] << 8) | bArr[13] + self.lockEvents = lockEvents + self.voltage = ((float)((bArr[14] << 8) | bArr[15])) * 0.01 + self.curr_state = (bArr[16] >> 4) & 3 + def parse_MQTT_advert(self, mqtt_advert): _LOGGER.debug("Parsing advert msg: %s", mqtt_advert) bArr = bytearray.fromhex(mqtt_advert)