From 145d9542d11e3b3706bf3942e96a935d66037589 Mon Sep 17 00:00:00 2001 From: C3H3-AI <256468226+C3H3-AI@users.noreply.github.com> Date: Sun, 13 Sep 2026 19:58:42 +0800 Subject: [PATCH 1/3] fix: two-phase heavy probe & backup field & fix model name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hub.py: switch-back probe is now two-phase — account-level (async_list_devices) followed by device-level (async_get_properties on the first discovered device). Prevents the primary identity from being activated while device-level requests are still 403-throttled, which caused rapid flip-flop between primary and backup. config_flow.py: expose backup_username / backup_password in the initial async_step_user schema (previously only available in the reconfigure step). sensor.py: rename DeviceModelType friendly name from '设备型号' (incorrect — it's a feature-composition enum) to '机型功能配置' (accurate — describes which hardware features exist). Also bump manifest version to 4.0.2. --- custom_components/hotata/config_flow.py | 8 ++++ custom_components/hotata/hub.py | 61 +++++++++++++++++++++---- custom_components/hotata/sensor.py | 2 +- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/custom_components/hotata/config_flow.py b/custom_components/hotata/config_flow.py index 5efd98e..57065a9 100644 --- a/custom_components/hotata/config_flow.py +++ b/custom_components/hotata/config_flow.py @@ -143,6 +143,12 @@ async def async_step_user( CONF_DESCENT_TIME, DEFAULT_DESCENT_TIME ) ), + CONF_BACKUP_USERNAME: user_input.get( + CONF_BACKUP_USERNAME, "" + ).strip(), + CONF_BACKUP_PASSWORD: user_input.get( + CONF_BACKUP_PASSWORD, "" + ), **creds, }, ) @@ -154,6 +160,8 @@ async def async_step_user( vol.Required( CONF_DESCENT_TIME, default=DEFAULT_DESCENT_TIME ): vol.All(vol.Coerce(int), vol.Range(min=0, max=20)), + vol.Optional(CONF_BACKUP_USERNAME): str, + vol.Optional(CONF_BACKUP_PASSWORD): str, } ) return self.async_show_form( diff --git a/custom_components/hotata/hub.py b/custom_components/hotata/hub.py index 3a0bdf2..63bb7ad 100644 --- a/custom_components/hotata/hub.py +++ b/custom_components/hotata/hub.py @@ -477,10 +477,10 @@ async def _switch_back_to_primary(self) -> bool: if not self._using_backup: return False try: - # Probe: account-level read (listBindings). A fresh IoT - # credential does NOT lift a throttled account's penalty, - # so verify with a real request before switching. - await self._primary_api.async_list_devices() + # Round 1: account-level read. A fresh IoT credential + # does NOT lift a throttled account's penalty, so + # verify the account is free before trying devices. + devices = await self._primary_api.async_list_devices() except HotataRateLimited: self._primary_penalty_until = ( time.time() + SWITCH_BACK_PROBE_DELAY @@ -488,11 +488,9 @@ async def _switch_back_to_primary(self) -> bool: self._switch_back_failures += 1 self._async_save_runtime_state() self._schedule_switch_back() - _LOGGER.info("Primary still rate-limited, staying on backup") + _LOGGER.info("Primary still rate-limited (account probe), staying on backup") return False except HotataAuthError as err: - # Credentials rejected — refresh was already attempted inside - # the client; retry later on the probe schedule. self._switch_back_failures += 1 self._schedule_switch_back() _LOGGER.warning("Primary probe auth failed: %s", err) @@ -500,9 +498,54 @@ async def _switch_back_to_primary(self) -> bool: except HotataError as err: self._switch_back_failures += 1 self._schedule_switch_back() - _LOGGER.warning("Primary probe failed: %s", err) + _LOGGER.warning("Primary account probe failed: %s", err) return False - # Probe OK — activate the primary identity. + + # Round 2: device-level probe. Account probe alone may pass + # when throttling only applies to device-level requests. + # Query properties of the first discovered device — this is + # the same heavy request the coordinator polls with, so if + # it 403s here we know the primary is still unfit for + # real traffic and we keep the backup. + if devices: + try: + await self._primary_api.async_get_properties( + devices[0].iot_id + ) + except HotataRateLimited: + self._primary_penalty_until = ( + time.time() + SWITCH_BACK_PROBE_DELAY + ) + self._switch_back_failures += 1 + self._async_save_runtime_state() + self._schedule_switch_back() + _LOGGER.info( + "Primary still rate-limited (device probe on %s), " + "staying on backup", + devices[0].iot_id, + ) + return False + except HotataAuthError as err: + self._switch_back_failures += 1 + self._schedule_switch_back() + _LOGGER.warning( + "Primary device probe auth failed on %s: %s", + devices[0].iot_id, + err, + ) + return False + except HotataError as err: + self._switch_back_failures += 1 + self._schedule_switch_back() + _LOGGER.warning( + "Primary device probe failed on %s: %s", + devices[0].iot_id, + err, + ) + return False + + # Heavy probe OK — the primary account can actually serve + # real traffic. Activate the primary identity. self._using_backup = False self._primary_penalty_until = 0.0 self._switch_back_failures = 0 diff --git a/custom_components/hotata/sensor.py b/custom_components/hotata/sensor.py index 41c35d8..d5c201d 100644 --- a/custom_components/hotata/sensor.py +++ b/custom_components/hotata/sensor.py @@ -108,7 +108,7 @@ def _sensor( ), _sensor( "DeviceModelType", - "设备型号", + "机型功能配置", value_map={ 0: "照明、消毒、风干、烘干", 1: "照明", From 6960c4f8a05dd17b3e407d4cad040f8083a523b5 Mon Sep 17 00:00:00 2001 From: C3H3-AI <256468226+C3H3-AI@users.noreply.github.com> Date: Sun, 13 Sep 2026 19:58:51 +0800 Subject: [PATCH 2/3] chore: bump manifest version to 4.0.2 --- custom_components/hotata/manifest.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/hotata/manifest.json b/custom_components/hotata/manifest.json index 71dbbbe..cefc0e7 100644 --- a/custom_components/hotata/manifest.json +++ b/custom_components/hotata/manifest.json @@ -1,4 +1,4 @@ -{ +{ "domain": "hotata", "name": "Hotata", "codeowners": [ @@ -12,5 +12,6 @@ "requirements": [ "cryptography>=42.0.0" ], - "version": "4.0.1" + "version": "4.0.2" } + From 331f21ba012ba396867d25cf1db28c97d195e146 Mon Sep 17 00:00:00 2001 From: C3H3-AI <256468226+C3H3-AI@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:01:29 +0800 Subject: [PATCH 3/3] fix: write manifest.json without BOM (PowerShell Set-Content added UTF-8 BOM, orjson rejects) --- custom_components/hotata/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/hotata/manifest.json b/custom_components/hotata/manifest.json index cefc0e7..abc0a8d 100644 --- a/custom_components/hotata/manifest.json +++ b/custom_components/hotata/manifest.json @@ -1,4 +1,4 @@ -{ +{ "domain": "hotata", "name": "Hotata", "codeowners": [