fix(R): 设备卡片开关状态改用后端最新DP, 修复面板操作后首页仍显示已关闭
- 根因: 卡片状态只读涂鸦 SDK 数据层的 dps, 而 Mesh 本地通道控制/子设备无云端推送时 SDK 不更新, 面板已打开首页仍显示已关闭 - 后端是状态汇聚点(APP回传/中控屏上报/涂鸦云订阅), 设备列表返回 capabilities.dps, APP 合并后端DP(物模型code归一化为dpId)覆盖SDK值 - APP 面板与首页开关控制成功后上报最新DP到后端并通知首页刷新卡片 - 中控屏上报的属性状态同步写入 device.capabilities.dps - 首页心跳(5s)同时轻量刷新设备列表, 感知其它入口(面板/中控屏)的设备状态变化
This commit is contained in:
parent
197f97ec1d
commit
a894013de7
@ -2,6 +2,8 @@ package com.xhai.iot.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 本平台后端设备(device 表),首页设备列表数据源。
|
||||
* 家庭/房间/设备清单由我们后端管理,涂鸦 SDK 仅用于设备控制。
|
||||
@ -35,6 +37,30 @@ public class BackendDevice {
|
||||
@SerializedName("isGateway")
|
||||
private Integer isGateway;
|
||||
|
||||
/** 设备能力描述(JSON),其中 dps 为后端掌握的最新 DP 值 */
|
||||
@SerializedName("capabilities")
|
||||
private Map<String, Object> capabilities;
|
||||
|
||||
public Map<String, Object> getCapabilities() { return capabilities; }
|
||||
public void setCapabilities(Map<String, Object> capabilities) { this.capabilities = capabilities; }
|
||||
|
||||
/**
|
||||
* 后端掌握的最新 DP 值。
|
||||
*
|
||||
* <p>后端是多来源的汇聚点:APP 控制回传、面板/中控屏上报、涂鸦云消息订阅都会写这里。
|
||||
* 涂鸦 SDK 数据层在「Mesh 本地通道控制」「子设备无云端推送」等场景下不会更新,
|
||||
* 所以卡片状态必须以后端这份数据为准。</p>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> getBackendDps() {
|
||||
if (capabilities == null) return null;
|
||||
Object dps = capabilities.get("dps");
|
||||
if (dps instanceof Map) {
|
||||
return (Map<String, Object>) dps;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getDeviceId() { return deviceId; }
|
||||
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
|
||||
|
||||
|
||||
@ -104,6 +104,9 @@ public class PlatformServiceManager {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
reportActivity(device, commands);
|
||||
// 同步最新 DP 值到后端并通知首页刷新卡片状态
|
||||
// (Mesh 本地通道控制时涂鸦 SDK 数据层不更新,首页会显示旧状态)
|
||||
reportDps(device, commands);
|
||||
if (callback != null) callback.onSuccess();
|
||||
}
|
||||
|
||||
@ -114,6 +117,40 @@ public class PlatformServiceManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上报控制后的最新 DP 值到后端(供首页/设备页显示开关状态)
|
||||
*
|
||||
* <p>后端是设备状态的汇聚点:APP 控制回传、面板·中控屏上报、涂鸦云消息订阅
|
||||
* 都写进同一处,卡片状态以后端这份数据为准。</p>
|
||||
*/
|
||||
private void reportDps(PlatformDevice device, Map<String, Object> commands) {
|
||||
try {
|
||||
Context ctx = context;
|
||||
if (ctx == null || device == null || commands == null || commands.isEmpty()) return;
|
||||
String auth = SessionManager.getInstance(ctx).getBearerToken();
|
||||
if (auth == null) return;
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("deviceId", device.getDeviceId());
|
||||
body.put("dps", commands);
|
||||
ApiClient.getApiService()
|
||||
.reportDeviceStatus(auth, body)
|
||||
.enqueue(new retrofit2.Callback<Result<Integer>>() {
|
||||
@Override
|
||||
public void onResponse(retrofit2.Call<Result<Integer>> call,
|
||||
retrofit2.Response<Result<Integer>> response) {
|
||||
com.xhai.iot.util.ActivityEventBus.notifyStateChanged(device.getDeviceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(retrofit2.Call<Result<Integer>> call, Throwable t) {
|
||||
Log.w(TAG, "上报DP状态失败: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "上报DP状态异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 上报控制动态到后端(失败静默,不影响控制) */
|
||||
private void reportActivity(PlatformDevice device, Map<String, Object> commands) {
|
||||
try {
|
||||
|
||||
@ -1071,6 +1071,9 @@ public class DeviceControlActivity extends AppCompatActivity {
|
||||
public void onSuccess() {
|
||||
// APP 走 SDK 直连不经后端,需主动上报,否则首页「控制记录」永远为空
|
||||
reportControlActivity(cmd);
|
||||
// 面板操作后把最新 DP 值同步到后端并通知首页刷新卡片状态
|
||||
// (Mesh 本地通道控制时涂鸦 SDK 数据层不会更新,只靠 SDK 会一直显示旧状态)
|
||||
reportDpsToBackend(cmd);
|
||||
runOnUiThread(() -> Toast.makeText(DeviceControlActivity.this,
|
||||
"指令已下发", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
@ -1137,6 +1140,42 @@ public class DeviceControlActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上报控制后的最新 DP 值到后端。
|
||||
*
|
||||
* <p>后端是设备状态的汇聚点(APP/面板/中控屏/涂鸦云订阅都写这里),首页卡片
|
||||
* 以后端这份数据为准。Mesh 本地通道控制时涂鸦 SDK 数据层不会更新,若不上报,
|
||||
* 首页会一直显示控制前的旧状态。</p>
|
||||
*/
|
||||
private void reportDpsToBackend(Map<String, Object> cmd) {
|
||||
try {
|
||||
String auth = com.xhai.iot.util.SessionManager.getInstance(this).getBearerToken();
|
||||
if (auth == null || cmd == null || cmd.isEmpty()) return;
|
||||
java.util.Map<String, Object> body = new java.util.HashMap<>();
|
||||
body.put("deviceId", deviceId);
|
||||
body.put("dps", cmd);
|
||||
com.xhai.iot.data.ApiClient.getApiService()
|
||||
.reportDeviceStatus(auth, body)
|
||||
.enqueue(new retrofit2.Callback<com.xhai.iot.model.Result<Integer>>() {
|
||||
@Override
|
||||
public void onResponse(retrofit2.Call<com.xhai.iot.model.Result<Integer>> call,
|
||||
retrofit2.Response<com.xhai.iot.model.Result<Integer>> response) {
|
||||
// 通知首页:设备状态已变化,刷新卡片上的开关状态
|
||||
com.xhai.iot.util.ActivityEventBus.notifyStateChanged(deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(retrofit2.Call<com.xhai.iot.model.Result<Integer>> call,
|
||||
Throwable t) {
|
||||
android.util.Log.w("DeviceControlActivity",
|
||||
"上报DP状态失败: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
android.util.Log.w("DeviceControlActivity", "上报DP状态异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 蓝牙 Mesh 子设备控制(走 Mesh 本地通道) */
|
||||
private void sendSigMeshCommand(DeviceBean dev, Map<String, Object> cmd, String json) {
|
||||
try {
|
||||
@ -1155,6 +1194,8 @@ public class DeviceControlActivity extends AppCompatActivity {
|
||||
public void onSuccess() {
|
||||
// APP 走 SDK 直连不经后端,需主动上报,否则首页「控制记录」永远为空
|
||||
reportControlActivity(cmd);
|
||||
// 面板操作后把最新 DP 值同步到后端并通知首页刷新卡片状态
|
||||
reportDpsToBackend(codeCmd);
|
||||
runOnUiThread(() -> Toast.makeText(DeviceControlActivity.this,
|
||||
"指令已下发", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@ -96,6 +96,8 @@ public class HomeFragment extends Fragment {
|
||||
TuyaSdkService.registerStatusCallback(deviceStatusCallback);
|
||||
// 注册动态事件回调:任意控制落库成功后,首页「近期动态」立即刷新
|
||||
ActivityEventBus.register(activityEventListener);
|
||||
// 注册设备状态回调:面板/中控屏操作后刷新卡片上的开关状态
|
||||
ActivityEventBus.registerStateListener(deviceStateListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -104,6 +106,7 @@ public class HomeFragment extends Fragment {
|
||||
unregisterAllTuyaListeners();
|
||||
TuyaSdkService.unregisterStatusCallback(deviceStatusCallback);
|
||||
ActivityEventBus.unregister(activityEventListener);
|
||||
ActivityEventBus.unregisterStateListener(deviceStateListener);
|
||||
if (activityRefreshTask != null) {
|
||||
activityRefreshHandler.removeCallbacks(activityRefreshTask);
|
||||
activityRefreshTask = null;
|
||||
@ -185,10 +188,11 @@ public class HomeFragment extends Fragment {
|
||||
*
|
||||
* <p>用途:面板/物理开关的操作,只有在手机能收到设备 DP 回流时才会被感知
|
||||
* (此时事件总线会立即通知,不必等心跳);但当操作发生在手机不在家/无蓝牙连接时,
|
||||
* 只有后端能感知(涂鸦云消息订阅打通后)。心跳作为兜底,让首页在停留期间
|
||||
* 也能把后端侧新产生的动态拉回来。</p>
|
||||
* 只有后端能感知。心跳作为兜底,让首页在停留期间也能把后端侧新产生的动态、
|
||||
* 以及其它入口(面板/中控屏)改动的设备状态拉回来。</p>
|
||||
*
|
||||
* <p>只刷新动态列表,不触碰设备列表,因此不会造成常用设备跳位。</p>
|
||||
* <p>设备列表走轻量刷新(不做探活/重注册监听),且顺序固定、值没变不重绑卡片,
|
||||
* 因此不会造成常用设备跳位或闪烁。</p>
|
||||
*/
|
||||
private static final long ACTIVITY_HEARTBEAT_MS = 5_000L;
|
||||
private final android.os.Handler heartbeatHandler =
|
||||
@ -197,7 +201,10 @@ public class HomeFragment extends Fragment {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isAdded() && getView() != null) {
|
||||
// 动态:后端侧产生的操控记录
|
||||
loadRecentActivities();
|
||||
// 设备状态:面板/中控屏等其它入口的操作(手机收不到 DP 回传时也能感知)
|
||||
loadDevicesFromBackend(false);
|
||||
}
|
||||
heartbeatHandler.postDelayed(this, ACTIVITY_HEARTBEAT_MS);
|
||||
}
|
||||
@ -275,6 +282,15 @@ public class HomeFragment extends Fragment {
|
||||
}
|
||||
};
|
||||
|
||||
/** 设备状态变化回调:面板/中控屏操作后刷新卡片开关状态 */
|
||||
private final ActivityEventBus.StateListener deviceStateListener = deviceId -> {
|
||||
if (!isAdded()) return;
|
||||
android.util.Log.i("HomeFragment", "设备状态变化,刷新卡片: deviceId=" + deviceId);
|
||||
if (rvDevices != null) {
|
||||
rvDevices.post(HomeFragment.this::scheduleDeviceRefresh);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 控制后延迟对齐(避免常用设备整表闪动) ====================
|
||||
|
||||
/** 用户刚点击控制的设备ID */
|
||||
@ -559,6 +575,16 @@ public class HomeFragment extends Fragment {
|
||||
|
||||
/** 从本平台后端拉取设备列表(按当前家庭过滤) */
|
||||
private void loadDevicesFromBackend() {
|
||||
loadDevicesFromBackend(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取设备列表
|
||||
*
|
||||
* @param full true = 完整刷新(探活、监听注册、室内温度等副作用);
|
||||
* false = 轻量刷新(只更新卡片状态,用于心跳等高频场景)
|
||||
*/
|
||||
private void loadDevicesFromBackend(boolean full) {
|
||||
// 已成功加载过就静默刷新,避免每次 DP 同步都闪一下加载圈
|
||||
showLoading(!devicesLoadedOnce);
|
||||
String auth = session.getBearerToken();
|
||||
@ -590,10 +616,14 @@ public class HomeFragment extends Fragment {
|
||||
updateQuickStats();
|
||||
return;
|
||||
}
|
||||
allDevices = toTuyaDevices(list);
|
||||
allDevices = toTuyaDevices(list, full);
|
||||
tvEmpty.setVisibility(View.GONE);
|
||||
adapter.update(allDevices);
|
||||
updateQuickStats();
|
||||
if (!full) {
|
||||
// 轻量刷新:只更新卡片状态,不做探活/监听注册等副作用
|
||||
return;
|
||||
}
|
||||
// 从涂鸦设备 DP 读取室内温湿度(温湿度传感器/空调)
|
||||
loadIndoorTemp();
|
||||
// 同步涂鸦默认家庭 gid 给 BizBundle(原生面板依赖 gid)
|
||||
@ -1206,7 +1236,7 @@ public class HomeFragment extends Fragment {
|
||||
}
|
||||
|
||||
/** 后端设备 → TuyaDevice(复用现有卡片 UI) */
|
||||
private List<TuyaDevice> toTuyaDevices(List<BackendDevice> list) {
|
||||
private List<TuyaDevice> toTuyaDevices(List<BackendDevice> list, boolean allowCorrection) {
|
||||
sdkDeviceDataMissing = false;
|
||||
List<TuyaDevice> result = new ArrayList<>();
|
||||
for (BackendDevice bd : list) {
|
||||
@ -1235,14 +1265,14 @@ public class HomeFragment extends Fragment {
|
||||
bd.getDeviceId(), devBean, bd.isOnline(), bd.getParentDeviceId());
|
||||
td.setOnline(sdkOnline);
|
||||
// 与后端不一致时回写纠正,保证下次进入/其他页面也准确
|
||||
if (sdkOnline != bd.isOnline()) {
|
||||
if (sdkOnline != bd.isOnline() && allowCorrection) {
|
||||
android.util.Log.i("HomeFragment", "在线状态纠正: " + bd.getDeviceName()
|
||||
+ " 后端=" + bd.getStatus() + " SDK=" + sdkOnline);
|
||||
reportStatusToBackend(bd.getDeviceId(), sdkOnline, null);
|
||||
}
|
||||
if (devBean != null) {
|
||||
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());
|
||||
@ -1259,6 +1289,11 @@ public class HomeFragment extends Fragment {
|
||||
android.util.Log.w("HomeFragment", "SDK状态读取异常 " + bd.getDeviceName(), e);
|
||||
}
|
||||
result.add(td);
|
||||
// DP 值合并:以后端为准。
|
||||
// 后端是多来源汇聚点(APP 控制回传 / 面板·中控屏上报 / 涂鸦云消息订阅都会写入),
|
||||
// 而涂鸦 SDK 数据层在「Mesh 本地通道控制」「子设备无云端推送」时不会更新,
|
||||
// 只用 SDK 的 dps 会出现「面板已打开,首页仍显示已关闭」。
|
||||
mergeBackendDps(bd, td);
|
||||
}
|
||||
// 1) 先按「上一次显示的顺序」重排:设备位置对用户是固定的,
|
||||
// 不能因为一次控制上报就让常用设备换位置(JD2BTSA 跳到 JD6BTSA 前面)。
|
||||
@ -1272,6 +1307,45 @@ public class HomeFragment extends Fragment {
|
||||
/** 上一次渲染的设备顺序(设备ID),用于保持列表位置稳定 */
|
||||
private final List<String> lastDeviceOrder = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 合并后端 DP 值到设备状态(后端优先)。
|
||||
*
|
||||
* <p>后端 DP 的键可能是涂鸦 dpId(如 "2"),也可能是物模型 code(如 "switch_2")
|
||||
* (Mesh 本地通道上报的是 code),这里统一归一化成 dpId,与 SDK 数据层对齐。</p>
|
||||
*/
|
||||
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;已是 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 = 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按上次顺序重排设备列表。
|
||||
*
|
||||
|
||||
@ -66,4 +66,51 @@ public final class ActivityEventBus {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 设备状态变化事件 ====================
|
||||
|
||||
/**
|
||||
* 设备状态变化监听
|
||||
*
|
||||
* <p>面板/中控屏操作或本地控制成功后触发,页面据此刷新设备卡片状态
|
||||
* (开关是开是关)。</p>
|
||||
*/
|
||||
public interface StateListener {
|
||||
void onDeviceStateChanged(String deviceId);
|
||||
}
|
||||
|
||||
private static final List<StateListener> stateListeners = new ArrayList<>();
|
||||
|
||||
public static void registerStateListener(StateListener listener) {
|
||||
if (listener == null) return;
|
||||
synchronized (stateListeners) {
|
||||
if (!stateListeners.contains(listener)) {
|
||||
stateListeners.add(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void unregisterStateListener(StateListener listener) {
|
||||
if (listener == null) return;
|
||||
synchronized (stateListeners) {
|
||||
stateListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/** 通知所有监听:设备状态已变化,刷新卡片状态 */
|
||||
public static void notifyStateChanged(String deviceId) {
|
||||
final List<StateListener> snapshot;
|
||||
synchronized (stateListeners) {
|
||||
if (stateListeners.isEmpty()) return;
|
||||
snapshot = new ArrayList<>(stateListeners);
|
||||
}
|
||||
MAIN.post(() -> {
|
||||
for (StateListener l : snapshot) {
|
||||
try {
|
||||
l.onDeviceStateChanged(deviceId);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +113,9 @@ public class PanelStateService {
|
||||
} else {
|
||||
deviceStateMapper.updateById(state);
|
||||
}
|
||||
// 同步到设备能力(dps):中控屏/面板改动的设备状态要让 APP 卡片读到,
|
||||
// 否则 APP 只能看到涂鸦 SDK 里的旧值(面板已打开、首页仍显示已关闭)。
|
||||
syncCapabilitiesDps(deviceId, propertyCode, value);
|
||||
}
|
||||
|
||||
PanelReportDto.PropertyResult result = new PanelReportDto.PropertyResult();
|
||||
@ -126,6 +129,43 @@ public class PanelStateService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把属性状态同步到 device.capabilities.dps。
|
||||
*
|
||||
* <p>device.capabilities 是 APP 读取设备最新 DP 的入口(APP 控制回传、涂鸦云
|
||||
* 消息订阅也写这里)。中控屏上报的属性(物模型 code)同样写进去,APP 端会把
|
||||
* code 归一化成涂鸦 dpId 后与 SDK 数据合并,保证卡片开关状态正确。</p>
|
||||
*/
|
||||
private void syncCapabilitiesDps(String deviceId, String propertyCode, Object value) {
|
||||
if (deviceId == null || propertyCode == null || propertyCode.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Device device = deviceMapper.selectByDeviceId(deviceId);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper =
|
||||
new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
Map<String, Object> caps = new java.util.LinkedHashMap<>();
|
||||
com.fasterxml.jackson.databind.JsonNode capsNode = device.getCapabilities();
|
||||
if (capsNode != null && capsNode.isObject()) {
|
||||
caps.putAll(mapper.convertValue(capsNode, Map.class));
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> dps = (caps.get("dps") instanceof Map)
|
||||
? (Map<String, Object>) caps.get("dps") : new java.util.LinkedHashMap<>();
|
||||
dps.put(propertyCode, value);
|
||||
caps.put("dps", dps);
|
||||
Device update = new Device();
|
||||
update.setId(device.getId());
|
||||
update.setCapabilities(mapper.valueToTree(caps));
|
||||
deviceMapper.updateById(update);
|
||||
} catch (Exception e) {
|
||||
log.warn("同步设备能力失败 deviceId={} property={}: {}", deviceId, propertyCode, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较状态新旧:世代更大者更新;同世代下序号更大者更新。
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user