feat(S): 支持多路开关设备(两位插座/四位排插)的状态判断与一键控制
- TuyaSdkService 新增 SwitchState/readSwitchState: 按电源开关通道聚合, 场景/背光/指示灯/倒计时等非电源开关按名称与分数排除 (JD6BTSA 4位开关+2位场景开关 -> 只识别4路电源开关; JD2BTSA -> 2路) - 卡片状态: 任一路开=已打开, 部分开=部分开启, 全关=已关闭 (此前只读第一个通道, 打开另一路时首页仍显示已关闭) - 卡片点击: 多路设备一键控制全部电源通道 - 乐观值锁定与快捷统计同步改为多路聚合 - 设备页(DevicesFragment/DeviceGridAdapter)同步合并后端最新DP与多路判断
This commit is contained in:
parent
a894013de7
commit
15534a3b11
@ -841,6 +841,98 @@ public class TuyaSdkService {
|
||||
private static final java.util.Map<String, java.util.Map<String, String>> DP_CODE_CACHE =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
// ==================== 多路开关聚合(两位插座 / 四位排插) ====================
|
||||
|
||||
/**
|
||||
* 设备开关聚合状态。
|
||||
*
|
||||
* <p>插座/排插有多路独立电源开关(JD2BTSA 两路、JD6BTSA 四路),
|
||||
* 卡片只有一个胶囊,必须明确"显示的是什么":
|
||||
* 任一路开 → 视为已打开;部分开 → 部分开启;全关 → 已关闭。</p>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取设备的开关聚合状态。
|
||||
*
|
||||
* <p>通道识别复用 {@link #findSwitchChannels(String)}(已排除场景、背光、指示灯、
|
||||
* 倒计时等非电源开关:JD6BTSA 的"4 位开关 + 2 位场景开关"只会取到 4 路电源开关)。</p>
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param dps DP 值集合(键为 dpId,兼容物模型 code)
|
||||
*/
|
||||
public static SwitchState readSwitchState(String deviceId, Map<String, Object> dps) {
|
||||
List<SwitchChannel> 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<String, Object> 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();
|
||||
|
||||
@ -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<String, Object> backendDps = bd.getBackendDps();
|
||||
if (backendDps == null || backendDps.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (td.getDps() == null) {
|
||||
td.setDps(new HashMap<>());
|
||||
}
|
||||
for (Map.Entry<String, Object> 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<String, String> idToCode =
|
||||
com.xhai.iot.service.TuyaSdkService.getDpCodeMap(deviceId);
|
||||
if (idToCode != null) {
|
||||
for (Map.Entry<String, String> 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);
|
||||
}
|
||||
|
||||
@ -695,17 +695,23 @@ public class HomeFragment extends Fragment {
|
||||
/** 判断设备开关 DP 是否为开 */
|
||||
private boolean isDeviceOn(TuyaDevice d) {
|
||||
try {
|
||||
// 多路插座/排插:任一路开即算开
|
||||
TuyaSdkService.SwitchState state =
|
||||
TuyaSdkService.readSwitchState(d.getId(), d.getDps());
|
||||
if (state.isKnown()) {
|
||||
return state.isAnyOn();
|
||||
}
|
||||
String dpId = TuyaSdkService.findSwitchDpId(d.getId());
|
||||
if (dpId == null) return false;
|
||||
if (dpId == null || d.getDps() == null) return false;
|
||||
Object v = d.getDps().get(dpId);
|
||||
if (v == null) {
|
||||
Map<String, Object> probe = new HashMap<>();
|
||||
probe.put(dpId, true);
|
||||
Map<String, Object> codeMap = TuyaSdkService.convertDpIdToCode(d.getId(), probe);
|
||||
if (codeMap == null || d.getDps() == null) return false;
|
||||
Object v = null;
|
||||
if (!codeMap.isEmpty()) {
|
||||
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;
|
||||
|
||||
@ -103,24 +103,18 @@ public class DeviceGridAdapter extends RecyclerView.Adapter<DeviceGridAdapter.Vi
|
||||
}
|
||||
|
||||
// 判断设备是否有可写的布尔(开关)DP;无开关功能的设备(如蓝牙网关)不显示开关
|
||||
final String switchDpId = TuyaSdkService.findSwitchDpId(device.getId());
|
||||
// 把数字 id 转换为涂鸦 code(dps map key 是 code),用于读真实开关状态
|
||||
String switchDpCode = null;
|
||||
if (switchDpId != null) {
|
||||
Map<String, Object> probe = new HashMap<>();
|
||||
probe.put(switchDpId, true);
|
||||
Map<String, Object> 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<TuyaSdkService.SwitchChannel> 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<DeviceGridAdapter.Vi
|
||||
"switch clicked: device=" + device.getId()
|
||||
+ " checked=" + isChecked + " dpId=" + switchDpId);
|
||||
Map<String, Object> commands = new HashMap<>();
|
||||
// 多路插座/排插:一键控制全部电源通道
|
||||
for (TuyaSdkService.SwitchChannel ch : switchChannels) {
|
||||
commands.put(ch.dpId, isChecked);
|
||||
}
|
||||
if (commands.isEmpty() && switchDpId != null) {
|
||||
commands.put(switchDpId, isChecked);
|
||||
}
|
||||
PlatformServiceManager.getInstance(holder.itemView.getContext())
|
||||
.controlDevice(asPlatformDevice(device), commands,
|
||||
new IPlatformService.ControlCallback() {
|
||||
|
||||
@ -124,20 +124,21 @@ public class HomeDeviceAdapter extends RecyclerView.Adapter<HomeDeviceAdapter.Vi
|
||||
pendingToggleAt.remove(d.getId());
|
||||
continue;
|
||||
}
|
||||
String switchDpId = TuyaSdkService.findSwitchDpId(d.getId());
|
||||
if (switchDpId == null) continue;
|
||||
// 多路开关:把所有电源通道都写成乐观值,保证卡片显示与点击一致
|
||||
List<TuyaSdkService.SwitchChannel> channels =
|
||||
TuyaSdkService.findSwitchChannels(d.getId());
|
||||
if (channels.isEmpty()) continue;
|
||||
Map<String, Object> dps = d.getDps();
|
||||
if (dps == null) {
|
||||
dps = new HashMap<>();
|
||||
d.setDps(dps);
|
||||
}
|
||||
Map<String, Object> probe = new HashMap<>();
|
||||
probe.put(switchDpId, true);
|
||||
Map<String, Object> codeMap = TuyaSdkService.convertDpIdToCode(d.getId(), probe);
|
||||
if (codeMap != null && !codeMap.isEmpty()) {
|
||||
dps.put(codeMap.keySet().iterator().next(), pending);
|
||||
for (TuyaSdkService.SwitchChannel ch : channels) {
|
||||
if (ch.code != null) {
|
||||
dps.put(ch.code, pending);
|
||||
}
|
||||
dps.put(ch.dpId, pending);
|
||||
}
|
||||
dps.put(switchDpId, pending);
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,28 +171,25 @@ public class HomeDeviceAdapter extends RecyclerView.Adapter<HomeDeviceAdapter.Vi
|
||||
}
|
||||
holder.ivIcon.setAlpha(online ? 1f : 0.45f);
|
||||
|
||||
// 开关 DP 逻辑(与 DeviceGridAdapter 一致)
|
||||
final String switchDpId = TuyaSdkService.findSwitchDpId(device.getId());
|
||||
String switchDpCode = null;
|
||||
if (switchDpId != null) {
|
||||
Map<String, Object> probe = new HashMap<>();
|
||||
probe.put(switchDpId, true);
|
||||
Map<String, Object> codeMap = TuyaSdkService.convertDpIdToCode(device.getId(), probe);
|
||||
if (codeMap != null && !codeMap.isEmpty()) {
|
||||
switchDpCode = codeMap.keySet().iterator().next();
|
||||
}
|
||||
}
|
||||
Object dpValue = null;
|
||||
if (switchDpCode != null && device.getDps() != null) {
|
||||
dpValue = device.getDps().get(switchDpCode);
|
||||
if (dpValue == null) dpValue = device.getDps().get(switchDpId);
|
||||
}
|
||||
// 开关 DP 逻辑(支持多路:两位插座 / 四位排插;
|
||||
// 场景、背光、指示灯等非电源开关已在 SDK 层按名称/分数排除)
|
||||
final List<TuyaSdkService.SwitchChannel> channels =
|
||||
TuyaSdkService.findSwitchChannels(device.getId());
|
||||
final boolean multiChannel = channels.size() > 1;
|
||||
final String switchDpId = channels.isEmpty() ? null : channels.get(0).dpId;
|
||||
|
||||
// 多路开关聚合状态:任一路开 → 已打开;部分开 → 部分开启;全关 → 已关闭
|
||||
TuyaSdkService.SwitchState switchState =
|
||||
TuyaSdkService.readSwitchState(device.getId(), device.getDps());
|
||||
Boolean dpValue = null;
|
||||
if (switchState.isKnown()) {
|
||||
dpValue = switchState.isAnyOn();
|
||||
} else if (device.getDps() != null) {
|
||||
// 无涂鸦 schema 的设备(平台虚拟/本地设备):从 dps 中兜底取常见开关字段
|
||||
if (dpValue == null && device.getDps() != null) {
|
||||
for (String key : new String[]{"switch_1", "switch", "switch1", "power", "1"}) {
|
||||
Object v = device.getDps().get(key);
|
||||
if (v instanceof Boolean) {
|
||||
dpValue = v;
|
||||
dpValue = (Boolean) v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -204,6 +202,9 @@ public class HomeDeviceAdapter extends RecyclerView.Adapter<HomeDeviceAdapter.Vi
|
||||
|
||||
// 刚点击且控制结果未回传时保持乐观值,避免被列表里的旧 dps 刷回造成胶囊回弹闪动
|
||||
final boolean shownOn = resolveShownState(device.getId(), isOn);
|
||||
// 多路设备部分开启时,文案区别于"已打开"
|
||||
final boolean shownPartial = multiChannel && shownOn
|
||||
&& !switchState.isAllOn() && !isPendingToggle(device.getId());
|
||||
|
||||
holder.pill.setAccentColor(accentColor);
|
||||
if (switchDpId == null) {
|
||||
@ -225,8 +226,8 @@ public class HomeDeviceAdapter extends RecyclerView.Adapter<HomeDeviceAdapter.Vi
|
||||
holder.pill.setEnabled(true);
|
||||
// 列表绑定/刷新不走动画(否则每次刷新胶囊都会动一下);动画只由用户点击触发
|
||||
holder.pill.setChecked(shownOn, false);
|
||||
// 在线开关类:在线(主题色)+ 已打开/已关闭
|
||||
holder.tvStatus.setText(buildStatusText(holder.tvStatus, true, true, shownOn));
|
||||
// 在线开关类:在线(主题色)+ 已打开/已关闭/部分开启
|
||||
holder.tvStatus.setText(buildStatusText(holder.tvStatus, true, true, shownOn, shownPartial));
|
||||
holder.pill.setOnUserToggleListener(checked -> {
|
||||
// 锁定乐观值:下发结果回传前,忽略列表刷新带回来的旧状态
|
||||
lockPendingToggle(device.getId(), checked);
|
||||
@ -235,7 +236,13 @@ public class HomeDeviceAdapter extends RecyclerView.Adapter<HomeDeviceAdapter.Vi
|
||||
controlStartListener.onControlStart(device.getId());
|
||||
}
|
||||
Map<String, Object> commands = new HashMap<>();
|
||||
// 多路插座/排插:一键控制全部电源通道(场景、背光等非电源开关不参与)
|
||||
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<HomeDeviceAdapter.Vi
|
||||
android.widget.Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
holder.tvStatus.setText(buildStatusText(holder.tvStatus, true, true, checked));
|
||||
holder.tvStatus.setText(buildStatusText(holder.tvStatus, true, true, checked, false));
|
||||
});
|
||||
}
|
||||
|
||||
@ -318,6 +325,16 @@ public class HomeDeviceAdapter extends RecyclerView.Adapter<HomeDeviceAdapter.Vi
|
||||
*/
|
||||
private CharSequence buildStatusText(View anchor, boolean online,
|
||||
boolean switchLike, boolean isOn) {
|
||||
return buildStatusText(anchor, online, switchLike, isOn, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态文案
|
||||
*
|
||||
* @param partial 多路设备部分开启(与"已打开"区分)
|
||||
*/
|
||||
private CharSequence buildStatusText(View anchor, boolean online,
|
||||
boolean switchLike, boolean isOn, boolean partial) {
|
||||
android.content.Context ctx = anchor.getContext();
|
||||
int muted = ctx.getColor(R.color.c_muted_fg);
|
||||
if (!online) {
|
||||
@ -332,17 +349,23 @@ public class HomeDeviceAdapter extends RecyclerView.Adapter<HomeDeviceAdapter.Vi
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return sp;
|
||||
}
|
||||
String full = "在线 · " + (isOn ? "已打开" : "已关闭");
|
||||
String full = "在线 · " + (partial ? "部分开启" : (isOn ? "已打开" : "已关闭"));
|
||||
SpannableString sp = new SpannableString(full);
|
||||
// 「在线」用主题色
|
||||
sp.setSpan(new ForegroundColorSpan(accentColor), 0, 2,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// 「已打开」用在线绿 / 「已关闭」用灰色
|
||||
// 「已打开/部分开启」用在线绿 / 「已关闭」用灰色
|
||||
sp.setSpan(new ForegroundColorSpan(isOn ? ctx.getColor(R.color.online) : muted),
|
||||
3, full.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return sp;
|
||||
}
|
||||
|
||||
/** 该设备是否处于"控制结果未回传"的乐观值窗口内 */
|
||||
private boolean isPendingToggle(String deviceId) {
|
||||
Long at = pendingToggleAt.get(deviceId);
|
||||
return at != null && System.currentTimeMillis() - at <= TOGGLE_LOCK_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备类型是否为「开关类」(可开/关的设备:灯、开关、插座、排插等)
|
||||
* 兼容本平台 device_type(light/socket/switch)与涂鸦 category(kg/cjkg/tdq/tgq...)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user