From 15534a3b11f9168bf4f05faf390ea6f9f3e41ab5 Mon Sep 17 00:00:00 2001 From: 2995840376 <2995840376@xiaomi.com> Date: Wed, 28 Oct 2026 11:41:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(S):=20=E6=94=AF=E6=8C=81=E5=A4=9A=E8=B7=AF?= =?UTF-8?q?=E5=BC=80=E5=85=B3=E8=AE=BE=E5=A4=87(=E4=B8=A4=E4=BD=8D?= =?UTF-8?q?=E6=8F=92=E5=BA=A7/=E5=9B=9B=E4=BD=8D=E6=8E=92=E6=8F=92)?= =?UTF-8?q?=E7=9A=84=E7=8A=B6=E6=80=81=E5=88=A4=E6=96=AD=E4=B8=8E=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TuyaSdkService 新增 SwitchState/readSwitchState: 按电源开关通道聚合, 场景/背光/指示灯/倒计时等非电源开关按名称与分数排除 (JD6BTSA 4位开关+2位场景开关 -> 只识别4路电源开关; JD2BTSA -> 2路) - 卡片状态: 任一路开=已打开, 部分开=部分开启, 全关=已关闭 (此前只读第一个通道, 打开另一路时首页仍显示已关闭) - 卡片点击: 多路设备一键控制全部电源通道 - 乐观值锁定与快捷统计同步改为多路聚合 - 设备页(DevicesFragment/DeviceGridAdapter)同步合并后端最新DP与多路判断 --- .../com/xhai/iot/service/TuyaSdkService.java | 92 +++++++++++++++++++ .../java/com/xhai/iot/ui/DevicesFragment.java | 40 +++++++- .../java/com/xhai/iot/ui/HomeFragment.java | 26 ++++-- .../iot/ui/adapter/DeviceGridAdapter.java | 38 ++++---- .../iot/ui/adapter/HomeDeviceAdapter.java | 89 +++++++++++------- 5 files changed, 222 insertions(+), 63 deletions(-) diff --git a/Android/app/src/main/java/com/xhai/iot/service/TuyaSdkService.java b/Android/app/src/main/java/com/xhai/iot/service/TuyaSdkService.java index b9e3b39..1361a1d 100644 --- a/Android/app/src/main/java/com/xhai/iot/service/TuyaSdkService.java +++ b/Android/app/src/main/java/com/xhai/iot/service/TuyaSdkService.java @@ -841,6 +841,98 @@ public class TuyaSdkService { private static final java.util.Map> DP_CODE_CACHE = new java.util.concurrent.ConcurrentHashMap<>(); + // ==================== 多路开关聚合(两位插座 / 四位排插) ==================== + + /** + * 设备开关聚合状态。 + * + *

插座/排插有多路独立电源开关(JD2BTSA 两路、JD6BTSA 四路), + * 卡片只有一个胶囊,必须明确"显示的是什么": + * 任一路开 → 视为已打开;部分开 → 部分开启;全关 → 已关闭。

+ */ + public static class SwitchState { + /** 识别到的电源开关通道数 */ + public final int channelCount; + /** 有明确取值的通道数 */ + public final int knownCount; + /** 已打开的通道数 */ + public final int onCount; + + SwitchState(int channelCount, int knownCount, int onCount) { + this.channelCount = channelCount; + this.knownCount = knownCount; + this.onCount = onCount; + } + + /** 是否至少有一路打开 */ + public boolean isAnyOn() { + return onCount > 0; + } + + /** 是否全部打开(已知通道全开) */ + public boolean isAllOn() { + return knownCount > 0 && onCount == knownCount; + } + + /** 是否"部分开启"(多路设备,既有开又有关) */ + public boolean isPartial() { + return channelCount > 1 && onCount > 0 && knownCount > 1 && onCount < knownCount; + } + + /** 是否有可用的状态取值 */ + public boolean isKnown() { + return knownCount > 0; + } + } + + /** + * 读取设备的开关聚合状态。 + * + *

通道识别复用 {@link #findSwitchChannels(String)}(已排除场景、背光、指示灯、 + * 倒计时等非电源开关:JD6BTSA 的"4 位开关 + 2 位场景开关"只会取到 4 路电源开关)。

+ * + * @param deviceId 设备ID + * @param dps DP 值集合(键为 dpId,兼容物模型 code) + */ + public static SwitchState readSwitchState(String deviceId, Map dps) { + List channels = findSwitchChannels(deviceId); + int known = 0; + int on = 0; + for (SwitchChannel ch : channels) { + Boolean v = readChannelValue(dps, ch); + if (v == null) continue; + known++; + if (v) on++; + } + return new SwitchState(channels.size(), known, on); + } + + /** 读取单个开关通道的值(dpId 优先,兼容 code 键与字符串型状态) */ + public static Boolean readChannelValue(Map dps, SwitchChannel ch) { + if (dps == null || dps.isEmpty() || ch == null) { + return null; + } + Object v = dps.get(ch.dpId); + if (v == null && ch.code != null) { + v = dps.get(ch.code); + } + if (v instanceof Boolean) { + return (Boolean) v; + } + if (v instanceof String) { + String s = ((String) v).trim(); + if ("true".equalsIgnoreCase(s) || "on".equalsIgnoreCase(s) + || "power_on".equalsIgnoreCase(s) || "opened".equalsIgnoreCase(s)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(s) || "off".equalsIgnoreCase(s) + || "power_off".equalsIgnoreCase(s) || "closed".equalsIgnoreCase(s)) { + return Boolean.FALSE; + } + } + return null; + } + /** 清除全部开关 DP 缓存(设备重新配网/schema 变化后调用) */ public static void clearSwitchDpCache() { SWITCH_DP_CACHE.clear(); diff --git a/Android/app/src/main/java/com/xhai/iot/ui/DevicesFragment.java b/Android/app/src/main/java/com/xhai/iot/ui/DevicesFragment.java index 5e71eb2..39fe66f 100644 --- a/Android/app/src/main/java/com/xhai/iot/ui/DevicesFragment.java +++ b/Android/app/src/main/java/com/xhai/iot/ui/DevicesFragment.java @@ -611,7 +611,7 @@ public class DevicesFragment extends Fragment { } } if (devBean.getDps() != null) { - td.setDps(devBean.getDps()); + td.setDps(new HashMap<>(devBean.getDps())); } if (devBean.getCategory() != null && !devBean.getCategory().isEmpty()) { td.setCategory(devBean.getCategory()); @@ -622,11 +622,49 @@ public class DevicesFragment extends Fragment { } } catch (Throwable ignored) { } + // 合并后端最新 DP:后端是状态汇聚点(APP 回传 / 面板·中控屏上报 / 涂鸦云订阅), + // 涂鸦 SDK 数据层在 Mesh 本地控制、子设备无云端推送时不会更新 + mergeBackendDps(bd, td); result.add(td); } return result; } + /** 合并后端 DP 值到设备状态(后端优先,code 归一化为 dpId) */ + private void mergeBackendDps(BackendDevice bd, TuyaDevice td) { + Map backendDps = bd.getBackendDps(); + if (backendDps == null || backendDps.isEmpty()) { + return; + } + if (td.getDps() == null) { + td.setDps(new HashMap<>()); + } + for (Map.Entry e : backendDps.entrySet()) { + String key = normalizeDpKey(bd.getDeviceId(), e.getKey()); + if (key == null) continue; + td.getDps().put(key, e.getValue()); + } + } + + /** DP 键归一化:物模型 code → 涂鸦 dpId */ + private String normalizeDpKey(String deviceId, String key) { + if (key == null || key.isEmpty()) return null; + if (key.matches("\\d+")) return key; + try { + Map idToCode = + com.xhai.iot.service.TuyaSdkService.getDpCodeMap(deviceId); + if (idToCode != null) { + for (Map.Entry e : idToCode.entrySet()) { + if (key.equals(e.getValue())) { + return e.getKey(); + } + } + } + } catch (Exception ignored) { + } + return key; + } + private void showLoading(boolean show) { progressBar.setVisibility(show ? View.VISIBLE : View.GONE); } diff --git a/Android/app/src/main/java/com/xhai/iot/ui/HomeFragment.java b/Android/app/src/main/java/com/xhai/iot/ui/HomeFragment.java index 4591bd4..673550d 100644 --- a/Android/app/src/main/java/com/xhai/iot/ui/HomeFragment.java +++ b/Android/app/src/main/java/com/xhai/iot/ui/HomeFragment.java @@ -695,17 +695,23 @@ public class HomeFragment extends Fragment { /** 判断设备开关 DP 是否为开 */ private boolean isDeviceOn(TuyaDevice d) { try { - String dpId = TuyaSdkService.findSwitchDpId(d.getId()); - if (dpId == null) return false; - Map probe = new HashMap<>(); - probe.put(dpId, true); - Map codeMap = TuyaSdkService.convertDpIdToCode(d.getId(), probe); - if (codeMap == null || d.getDps() == null) return false; - Object v = null; - if (!codeMap.isEmpty()) { - v = d.getDps().get(codeMap.keySet().iterator().next()); + // 多路插座/排插:任一路开即算开 + TuyaSdkService.SwitchState state = + TuyaSdkService.readSwitchState(d.getId(), d.getDps()); + if (state.isKnown()) { + return state.isAnyOn(); + } + String dpId = TuyaSdkService.findSwitchDpId(d.getId()); + if (dpId == null || d.getDps() == null) return false; + Object v = d.getDps().get(dpId); + if (v == null) { + Map probe = new HashMap<>(); + probe.put(dpId, true); + Map codeMap = TuyaSdkService.convertDpIdToCode(d.getId(), probe); + if (codeMap != null && !codeMap.isEmpty()) { + v = d.getDps().get(codeMap.keySet().iterator().next()); + } } - if (v == null) v = d.getDps().get(dpId); return Boolean.TRUE.equals(v); } catch (Throwable e) { return false; diff --git a/Android/app/src/main/java/com/xhai/iot/ui/adapter/DeviceGridAdapter.java b/Android/app/src/main/java/com/xhai/iot/ui/adapter/DeviceGridAdapter.java index 51f720b..1726ad3 100644 --- a/Android/app/src/main/java/com/xhai/iot/ui/adapter/DeviceGridAdapter.java +++ b/Android/app/src/main/java/com/xhai/iot/ui/adapter/DeviceGridAdapter.java @@ -103,24 +103,18 @@ public class DeviceGridAdapter extends RecyclerView.Adapter probe = new HashMap<>(); - probe.put(switchDpId, true); - Map codeMap = TuyaSdkService.convertDpIdToCode(device.getId(), probe); - if (codeMap != null && !codeMap.isEmpty()) { - switchDpCode = codeMap.keySet().iterator().next(); - } - } - // 从 dps 读取设备真实开关状态(同步显示) - Object dpValue = null; - if (switchDpCode != null && device.getDps() != null) { - dpValue = device.getDps().get(switchDpCode); - if (dpValue == null) dpValue = device.getDps().get(switchDpId); - } - final boolean initialChecked = Boolean.TRUE.equals(dpValue); + // 多路设备(两位插座/四位排插):取全部"电源开关"通道, + // 场景、背光、指示灯等非电源开关已在 SDK 层按名称/分数排除 + final List switchChannels = + TuyaSdkService.findSwitchChannels(device.getId()); + final String switchDpId = switchChannels.isEmpty() ? null : switchChannels.get(0).dpId; + // 聚合状态:任一路开 → 视为已打开 + TuyaSdkService.SwitchState switchState = + TuyaSdkService.readSwitchState(device.getId(), device.getDps()); + final boolean initialChecked = switchState.isKnown() + ? switchState.isAnyOn() + : Boolean.TRUE.equals(device.getDps() == null ? null + : device.getDps().get(switchDpId)); if (switchDpId == null || !online) { // 无开关功能或离线:隐藏/禁用开关,避免网关等设备出现无效开关 @@ -143,7 +137,13 @@ public class DeviceGridAdapter extends RecyclerView.Adapter { // 锁定乐观值:下发结果回传前,忽略列表刷新带回来的旧状态 lockPendingToggle(device.getId(), checked); @@ -235,7 +236,13 @@ public class HomeDeviceAdapter extends RecyclerView.Adapter commands = new HashMap<>(); - commands.put(switchDpId, checked); + // 多路插座/排插:一键控制全部电源通道(场景、背光等非电源开关不参与) + for (TuyaSdkService.SwitchChannel ch : channels) { + commands.put(ch.dpId, checked); + } + if (commands.isEmpty() && switchDpId != null) { + commands.put(switchDpId, checked); + } PlatformServiceManager.getInstance(holder.itemView.getContext()) .controlDevice(asPlatformDevice(device), commands, new IPlatformService.ControlCallback() { @@ -253,7 +260,7 @@ public class HomeDeviceAdapter extends RecyclerView.Adapter