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)." + } + } + } + } +}