Lock cycle transitions patch (vs fd50-gatt-fallback)
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
diff --git a/custom_components/tuya_local_ble/const.py b/custom_components/tuya_local_ble/const.py
|
||||
index 1bebccd..2b13c6e 100644
|
||||
--- a/custom_components/tuya_local_ble/const.py
|
||||
+++ b/custom_components/tuya_local_ble/const.py
|
||||
@@ -18,6 +18,8 @@ CONF_DEVICE_NAME: Final = "device_name"
|
||||
CONF_PRODUCT_MODEL: Final = "product_model"
|
||||
CONF_PRODUCT_NAME: Final = "product_name"
|
||||
CONF_BLE_UNLOCK_CHECK: Final = "ble_unlock_check"
|
||||
+CONF_LOCK_CYCLE_SECONDS: Final = "lock_cycle_seconds"
|
||||
+CONF_UNLOCK_CYCLE_SECONDS: Final = "unlock_cycle_seconds"
|
||||
|
||||
CONF_CRED_FILE = DOMAIN + "/devices.json"
|
||||
|
||||
diff --git a/custom_components/tuya_local_ble/keyman.py b/custom_components/tuya_local_ble/keyman.py
|
||||
index e80d7e6..ea2d4ef 100644
|
||||
--- a/custom_components/tuya_local_ble/keyman.py
|
||||
+++ b/custom_components/tuya_local_ble/keyman.py
|
||||
@@ -26,6 +26,8 @@ from .const import (
|
||||
CONF_DEVICE_NAME,
|
||||
CONF_PRODUCT_NAME,
|
||||
CONF_BLE_UNLOCK_CHECK,
|
||||
+ CONF_LOCK_CYCLE_SECONDS,
|
||||
+ CONF_UNLOCK_CYCLE_SECONDS,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
@@ -41,6 +43,8 @@ CONF_TUYA_DEVICE_KEYS = [
|
||||
CONF_PRODUCT_NAME,
|
||||
CONF_PRODUCT_MODEL,
|
||||
CONF_BLE_UNLOCK_CHECK,
|
||||
+ CONF_LOCK_CYCLE_SECONDS,
|
||||
+ CONF_UNLOCK_CYCLE_SECONDS,
|
||||
]
|
||||
|
||||
class HASSTuyaBLEDeviceManager(AbstaractTuyaBLEDeviceManager):
|
||||
@@ -90,6 +94,8 @@ class HASSTuyaBLEDeviceManager(AbstaractTuyaBLEDeviceManager):
|
||||
credentials.get(CONF_PRODUCT_MODEL, ""),
|
||||
credentials.get(CONF_PRODUCT_NAME, ""),
|
||||
credentials.get(CONF_BLE_UNLOCK_CHECK, ""),
|
||||
+ credentials.get(CONF_LOCK_CYCLE_SECONDS),
|
||||
+ credentials.get(CONF_UNLOCK_CYCLE_SECONDS),
|
||||
)
|
||||
_LOGGER.debug("Retrieved: %s", result)
|
||||
|
||||
diff --git a/custom_components/tuya_local_ble/lock.py b/custom_components/tuya_local_ble/lock.py
|
||||
index 1f91275..89ab339 100644
|
||||
--- a/custom_components/tuya_local_ble/lock.py
|
||||
+++ b/custom_components/tuya_local_ble/lock.py
|
||||
@@ -17,6 +17,7 @@ from homeassistant.components.lock import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
+from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from homeassistant.const import (
|
||||
@@ -45,6 +46,12 @@ class TuyaBLELockMapping:
|
||||
keep_connect: bool = False
|
||||
dp_type: TuyaBLEDataPointType | None = None
|
||||
is_available: TuyaBLELockIsAvailable = None
|
||||
+ # How long the motor takes to finish a full lock/unlock cycle. Commands
|
||||
+ # sent mid-cycle are acknowledged (beep) but not executed by the lock, so
|
||||
+ # the entity rejects them until the cycle completes. Can be overridden
|
||||
+ # per device in devices.json (lock_cycle_seconds / unlock_cycle_seconds).
|
||||
+ lock_cycle_seconds: float = 8.0
|
||||
+ unlock_cycle_seconds: float = 8.0
|
||||
|
||||
@dataclass
|
||||
class TuyaBLELockMapping(TuyaBLELockMapping):
|
||||
@@ -135,6 +142,18 @@ class TuyaBLELock(TuyaBLEEntity, LockEntity):
|
||||
self._commanded_timer = None
|
||||
self._datapoint_nop = None
|
||||
self._isjammed = False
|
||||
+ self._cycle_active = False
|
||||
+ self._cycle_cancel = None
|
||||
+ device_lock_cycle = device.lock_cycle_seconds
|
||||
+ device_unlock_cycle = device.unlock_cycle_seconds
|
||||
+ self._lock_cycle_seconds = (
|
||||
+ float(device_lock_cycle) if device_lock_cycle
|
||||
+ else mapping.lock_cycle_seconds
|
||||
+ )
|
||||
+ self._unlock_cycle_seconds = (
|
||||
+ float(device_unlock_cycle) if device_unlock_cycle
|
||||
+ else mapping.unlock_cycle_seconds
|
||||
+ )
|
||||
self._update_attrs()
|
||||
if mapping.keep_connect:
|
||||
self._thread = Timer(self._mapping.keep_connect_timer, self.send_nop_request)
|
||||
@@ -162,18 +181,18 @@ class TuyaBLELock(TuyaBLEEntity, LockEntity):
|
||||
def is_locking(self) -> bool:
|
||||
"""Return true if device is locking."""
|
||||
return (
|
||||
- self._current_state == LockState.UNLOCKED
|
||||
+ self._commanded
|
||||
and self._target_state == LockState.LOCKED
|
||||
- and self._commanded
|
||||
+ and self._current_state != LockState.LOCKED
|
||||
)
|
||||
|
||||
@property
|
||||
def is_unlocking(self) -> bool:
|
||||
"""Return true if device is unlocking."""
|
||||
return (
|
||||
- self._current_state == LockState.LOCKED
|
||||
+ self._commanded
|
||||
and self._target_state == LockState.UNLOCKED
|
||||
- and self._commanded
|
||||
+ and self._current_state != LockState.UNLOCKED
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -202,6 +221,16 @@ class TuyaBLELock(TuyaBLEEntity, LockEntity):
|
||||
await self._set_lock_state(LockState.UNLOCKED)
|
||||
|
||||
async def _set_lock_state(self, state: str) -> None:
|
||||
+ if self._cycle_active:
|
||||
+ # The lock beeps on commands received mid-cycle but does not
|
||||
+ # execute them, while HA would flip its state — reject instead.
|
||||
+ _LOGGER.debug(
|
||||
+ "%s: lock cycle in progress, ignoring %s command",
|
||||
+ self._device.address,
|
||||
+ state,
|
||||
+ )
|
||||
+ return
|
||||
+
|
||||
self._target_state = state
|
||||
self._update_attrs()
|
||||
self.async_write_ha_state()
|
||||
@@ -217,22 +246,30 @@ class TuyaBLELock(TuyaBLEEntity, LockEntity):
|
||||
False,
|
||||
)
|
||||
|
||||
- if self._device.product_id == "hc7n0urm" and self._target_state == LockState.UNLOCKED:
|
||||
- await datapoint.set_value(True)
|
||||
- self._current_state = LockState.UNLOCKED
|
||||
- self._commanded = False
|
||||
- self._isjammed = False
|
||||
+ if self._device.product_id == "hc7n0urm":
|
||||
+ if self._target_state == LockState.UNLOCKED:
|
||||
+ cycle_seconds = self._unlock_cycle_seconds
|
||||
+ else:
|
||||
+ cycle_seconds = self._lock_cycle_seconds
|
||||
+ self._cycle_active = True
|
||||
+ self._commanded = True
|
||||
+ self._commanded_timer = datetime.now()
|
||||
self._update_attrs()
|
||||
self.async_write_ha_state()
|
||||
- return
|
||||
-
|
||||
- if self._device.product_id == "hc7n0urm" and self._target_state == LockState.LOCKED:
|
||||
- await datapoint.set_value(True)
|
||||
- self._current_state = LockState.LOCKED
|
||||
- self._commanded = False
|
||||
+ try:
|
||||
+ await datapoint.set_value(True)
|
||||
+ except Exception:
|
||||
+ self._cycle_active = False
|
||||
+ self._commanded = False
|
||||
+ self._update_attrs()
|
||||
+ self.async_write_ha_state()
|
||||
+ raise
|
||||
self._isjammed = False
|
||||
- self._update_attrs()
|
||||
- self.async_write_ha_state()
|
||||
+ # The device ACK only confirms the command was accepted; the motor
|
||||
+ # is still turning, so commit the final state after the cycle time.
|
||||
+ self._cycle_cancel = async_call_later(
|
||||
+ self._hass, cycle_seconds, self._finish_cycle
|
||||
+ )
|
||||
return
|
||||
|
||||
#Gimdow need true to activate lock/unlock commands
|
||||
@@ -240,6 +277,24 @@ class TuyaBLELock(TuyaBLEEntity, LockEntity):
|
||||
self._commanded = True
|
||||
self._commanded_timer = datetime.now()
|
||||
|
||||
+ @callback
|
||||
+ def _finish_cycle(self, _now) -> None:
|
||||
+ """Commit the target state once the motor cycle is over."""
|
||||
+ self._cycle_cancel = None
|
||||
+ self._cycle_active = False
|
||||
+ if self._commanded and self._target_state is not None:
|
||||
+ self._current_state = self._target_state
|
||||
+ self._commanded = False
|
||||
+ self._update_attrs()
|
||||
+ self.async_write_ha_state()
|
||||
+
|
||||
+ async def async_will_remove_from_hass(self) -> None:
|
||||
+ """Cancel a pending cycle timer on removal."""
|
||||
+ if self._cycle_cancel is not None:
|
||||
+ self._cycle_cancel()
|
||||
+ self._cycle_cancel = None
|
||||
+ await super().async_will_remove_from_hass()
|
||||
+
|
||||
|
||||
def update_device_state(self):
|
||||
datapoint = self._device.datapoints[self._mapping.dp_id]
|
||||
diff --git a/custom_components/tuya_local_ble/tuya_ble/manager.py b/custom_components/tuya_local_ble/tuya_ble/manager.py
|
||||
index f789a53..ba4b12b 100644
|
||||
--- a/custom_components/tuya_local_ble/tuya_ble/manager.py
|
||||
+++ b/custom_components/tuya_local_ble/tuya_ble/manager.py
|
||||
@@ -15,6 +15,8 @@ class TuyaBLEDeviceCredentials:
|
||||
product_model: str | None
|
||||
product_name: str | None
|
||||
ble_unlock_check: str | None = None
|
||||
+ lock_cycle_seconds: float | None = None
|
||||
+ unlock_cycle_seconds: float | None = None
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
@@ -26,7 +28,9 @@ class TuyaBLEDeviceCredentials:
|
||||
"device_name: %s, "
|
||||
"product_model: %s, "
|
||||
"product_name: %s, "
|
||||
- "ble_unlock_check: %s"
|
||||
+ "ble_unlock_check: %s, "
|
||||
+ "lock_cycle_seconds: %s, "
|
||||
+ "unlock_cycle_seconds: %s"
|
||||
) % (
|
||||
self.category,
|
||||
self.product_id,
|
||||
@@ -34,6 +38,8 @@ class TuyaBLEDeviceCredentials:
|
||||
self.product_model,
|
||||
self.product_name,
|
||||
"set" if self.ble_unlock_check else "not set",
|
||||
+ self.lock_cycle_seconds,
|
||||
+ self.unlock_cycle_seconds,
|
||||
)
|
||||
|
||||
class AbstaractTuyaBLEDeviceManager(ABC):
|
||||
diff --git a/custom_components/tuya_local_ble/tuya_ble/tuya_ble.py b/custom_components/tuya_local_ble/tuya_ble/tuya_ble.py
|
||||
index 73049eb..55bd644 100644
|
||||
--- a/custom_components/tuya_local_ble/tuya_ble/tuya_ble.py
|
||||
+++ b/custom_components/tuya_local_ble/tuya_ble/tuya_ble.py
|
||||
@@ -416,6 +416,18 @@ class TuyaBLEDevice:
|
||||
else:
|
||||
return ""
|
||||
|
||||
+ @property
|
||||
+ def lock_cycle_seconds(self) -> float | None:
|
||||
+ if self._device_info is not None:
|
||||
+ return self._device_info.lock_cycle_seconds
|
||||
+ return None
|
||||
+
|
||||
+ @property
|
||||
+ def unlock_cycle_seconds(self) -> float | None:
|
||||
+ if self._device_info is not None:
|
||||
+ return self._device_info.unlock_cycle_seconds
|
||||
+ return None
|
||||
+
|
||||
@property
|
||||
def device_version(self) -> str:
|
||||
return self._device_version
|
||||
Reference in New Issue
Block a user