diff --git a/options-flow.patch b/options-flow.patch new file mode 100644 index 0000000..e546274 --- /dev/null +++ b/options-flow.patch @@ -0,0 +1,270 @@ +diff --git a/custom_components/tuya_local_ble/__init__.py b/custom_components/tuya_local_ble/__init__.py +index d000ccd..d1a060b 100644 +--- a/custom_components/tuya_local_ble/__init__.py ++++ b/custom_components/tuya_local_ble/__init__.py +@@ -89,7 +89,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) +- #entry.async_on_unload(entry.add_update_listener(_async_update_listener)) ++ entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + + async def _async_stop(event: Event) -> None: + """Close the connection.""" +@@ -100,6 +100,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + ) + return True + ++async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: ++ """Reload the entry when options change.""" ++ await hass.config_entries.async_reload(entry.entry_id) ++ ++ + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): +diff --git a/custom_components/tuya_local_ble/config_flow.py b/custom_components/tuya_local_ble/config_flow.py +index a89eff8..d93947a 100644 +--- a/custom_components/tuya_local_ble/config_flow.py ++++ b/custom_components/tuya_local_ble/config_flow.py +@@ -8,8 +8,10 @@ from typing import Any + import voluptuous as vol + + from homeassistant.config_entries import ( ++ ConfigEntry, + ConfigFlow, +- ConfigFlowResult ++ ConfigFlowResult, ++ OptionsFlow, + ) + from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, +@@ -18,22 +20,45 @@ from homeassistant.components.bluetooth import ( + from homeassistant.const import CONF_ADDRESS + from homeassistant.core import callback + #from homeassistant.data_entry_flow import FlowResult ++from homeassistant.helpers.selector import ( ++ NumberSelector, ++ NumberSelectorConfig, ++ NumberSelectorMode, ++ TextSelector, ++ TextSelectorConfig, ++) + + from .tuya_ble import SERVICE_UUID, TuyaBLEDeviceCredentials + + from .const import ( + DOMAIN, ++ CONF_BLE_UNLOCK_CHECK, ++ CONF_LOCK_CYCLE_SECONDS, ++ CONF_UNLOCK_CYCLE_SECONDS, + ) + from .devices import TuyaBLEData, get_device_readable_name + from .keyman import HASSTuyaBLEDeviceManager + + _LOGGER = logging.getLogger(__name__) + ++OPTIONS_KEYS = ( ++ CONF_LOCK_CYCLE_SECONDS, ++ CONF_UNLOCK_CYCLE_SECONDS, ++ CONF_BLE_UNLOCK_CHECK, ++) ++ ++ + class TuyaBLEConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Tuya BLE.""" + + VERSION = 1 + ++ @staticmethod ++ @callback ++ def async_get_options_flow(config_entry: ConfigEntry) -> TuyaBLEOptionsFlow: ++ """Get the options flow for this handler.""" ++ return TuyaBLEOptionsFlow() ++ + def __init__(self) -> None: + """Initialize the config flow.""" + super().__init__() +@@ -132,3 +157,61 @@ class TuyaBLEConfigFlow(ConfigFlow, domain=DOMAIN): + ), + errors=errors, + ) ++ ++ ++class TuyaBLEOptionsFlow(OptionsFlow): ++ """Handle options for a Tuya BLE device. ++ ++ Values set here override the matching fields in devices.json; an empty ++ field falls back to devices.json (or the built-in default). ++ """ ++ ++ async def async_step_init( ++ self, user_input: dict[str, Any] | None = None ++ ) -> ConfigFlowResult: ++ """Manage the device options.""" ++ if user_input is not None: ++ options = dict(self.config_entry.options) ++ for key in OPTIONS_KEYS: ++ value = user_input.get(key) ++ if value in (None, ""): ++ options.pop(key, None) ++ else: ++ options[key] = value ++ return self.async_create_entry(title="", data=options) ++ ++ options = self.config_entry.options ++ cycle_selector = NumberSelector( ++ NumberSelectorConfig( ++ min=1, ++ max=120, ++ step=0.1, ++ unit_of_measurement="s", ++ mode=NumberSelectorMode.BOX, ++ ) ++ ) ++ return self.async_show_form( ++ step_id="init", ++ data_schema=vol.Schema( ++ { ++ vol.Optional( ++ CONF_LOCK_CYCLE_SECONDS, ++ description={ ++ "suggested_value": options.get(CONF_LOCK_CYCLE_SECONDS) ++ }, ++ ): cycle_selector, ++ vol.Optional( ++ CONF_UNLOCK_CYCLE_SECONDS, ++ description={ ++ "suggested_value": options.get(CONF_UNLOCK_CYCLE_SECONDS) ++ }, ++ ): cycle_selector, ++ vol.Optional( ++ CONF_BLE_UNLOCK_CHECK, ++ description={ ++ "suggested_value": options.get(CONF_BLE_UNLOCK_CHECK) ++ }, ++ ): TextSelector(TextSelectorConfig()), ++ }, ++ ), ++ ) +diff --git a/custom_components/tuya_local_ble/keyman.py b/custom_components/tuya_local_ble/keyman.py +index ea2d4ef..02e4e73 100644 +--- a/custom_components/tuya_local_ble/keyman.py ++++ b/custom_components/tuya_local_ble/keyman.py +@@ -84,6 +84,15 @@ class HASSTuyaBLEDeviceManager(AbstaractTuyaBLEDeviceManager): + credentials = self._devicedata.get(address) + + if credentials: ++ # Options set via the integration UI override devices.json values. ++ for key in ( ++ CONF_BLE_UNLOCK_CHECK, ++ CONF_LOCK_CYCLE_SECONDS, ++ CONF_UNLOCK_CYCLE_SECONDS, ++ ): ++ value = self._data.get(key) ++ if value not in (None, ""): ++ credentials = {**credentials, key: value} + result = TuyaBLEDeviceCredentials( + credentials.get(CONF_UUID, ""), + credentials.get(CONF_LOCAL_KEY, ""), +diff --git a/custom_components/tuya_local_ble/strings.json b/custom_components/tuya_local_ble/strings.json +index 76f3fb2..ef92e87 100644 +--- a/custom_components/tuya_local_ble/strings.json ++++ b/custom_components/tuya_local_ble/strings.json +@@ -210,5 +210,23 @@ + "name": "Program: position[/time];..." + } + } ++ }, ++ "options": { ++ "step": { ++ "init": { ++ "title": "Tuya Local BLE options", ++ "description": "These values override the matching fields in config/tuya_local_ble/devices.json. Leave a field empty to fall back to devices.json (or the built-in default). If the device stops working after a change, clear the fields here or fix devices.json directly.", ++ "data": { ++ "lock_cycle_seconds": "Lock cycle time (s)", ++ "unlock_cycle_seconds": "Unlock cycle time (s)", ++ "ble_unlock_check": "ble_unlock_check (base64)" ++ }, ++ "data_description": { ++ "lock_cycle_seconds": "How long the motor runs when locking, including the pause at the end stop. New commands are rejected while the cycle is running.", ++ "unlock_cycle_seconds": "Same as above, for unlocking.", ++ "ble_unlock_check": "Raw Tuya status value used to authorize remote unlock. Refresh it from the Tuya IoT cloud (Get Status Reporting Log) if unlock starts returning error code 1." ++ } ++ } ++ } + } + } +diff --git a/custom_components/tuya_local_ble/translations/en.json b/custom_components/tuya_local_ble/translations/en.json +index 019f00e..68d63af 100644 +--- a/custom_components/tuya_local_ble/translations/en.json ++++ b/custom_components/tuya_local_ble/translations/en.json +@@ -209,6 +209,23 @@ + "name": "Program: position[/time];..." + } + } ++ }, ++ "options": { ++ "step": { ++ "init": { ++ "title": "Tuya Local BLE options", ++ "description": "These values override the matching fields in config/tuya_local_ble/devices.json. Leave a field empty to fall back to devices.json (or the built-in default). If the device stops working after a change, clear the fields here or fix devices.json directly.", ++ "data": { ++ "lock_cycle_seconds": "Lock cycle time (s)", ++ "unlock_cycle_seconds": "Unlock cycle time (s)", ++ "ble_unlock_check": "ble_unlock_check (base64)" ++ }, ++ "data_description": { ++ "lock_cycle_seconds": "How long the motor runs when locking, including the pause at the end stop. New commands are rejected while the cycle is running.", ++ "unlock_cycle_seconds": "Same as above, for unlocking.", ++ "ble_unlock_check": "Raw Tuya status value used to authorize remote unlock. Refresh it from the Tuya IoT cloud (Get Status Reporting Log) if unlock starts returning error code 1." ++ } ++ } + } + } +- +\ No newline at end of file ++} +diff --git a/custom_components/tuya_local_ble/translations/ru.json b/custom_components/tuya_local_ble/translations/ru.json +new file mode 100644 +index 0000000..e976df9 +--- /dev/null ++++ b/custom_components/tuya_local_ble/translations/ru.json +@@ -0,0 +1,37 @@ ++{ ++ "config": { ++ "abort": { ++ "no_unconfigured_devices": "Ненастроенные устройства не найдены." ++ }, ++ "error": { ++ "device_not_registered": "Устройство не зарегистрировано, проверьте конфигурацию." ++ }, ++ "flow_title": "{name}", ++ "step": { ++ "device": { ++ "data": { ++ "address": "Устройство Tuya BLE" ++ }, ++ "description": "Выберите устройство Tuya BLE для настройки. Устройство должно быть зарегистрировано в облаке через мобильное приложение." ++ } ++ } ++ }, ++ "options": { ++ "step": { ++ "init": { ++ "title": "Настройки Tuya Local BLE", ++ "description": "Эти значения перекрывают одноимённые поля в config/tuya_local_ble/devices.json. Пустое поле — используется значение из devices.json (или встроенное по умолчанию). Если после изменения устройство перестало работать — очистите поля здесь или поправьте devices.json напрямую.", ++ "data": { ++ "lock_cycle_seconds": "Время цикла закрытия (с)", ++ "unlock_cycle_seconds": "Время цикла открытия (с)", ++ "ble_unlock_check": "ble_unlock_check (base64)" ++ }, ++ "data_description": { ++ "lock_cycle_seconds": "Сколько работает мотор при закрытии, включая паузу после упора. Пока идёт цикл, повторные команды отклоняются.", ++ "unlock_cycle_seconds": "То же самое, но для открытия.", ++ "ble_unlock_check": "Сырое значение статуса Tuya для авторизации удалённого открытия. Если открытие начало возвращать error code 1 — обновите значение из облака Tuya (Get Status Reporting Log)." ++ } ++ } ++ } ++ } ++}