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 com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 本平台后端设备(device 表),首页设备列表数据源。
|
* 本平台后端设备(device 表),首页设备列表数据源。
|
||||||
* 家庭/房间/设备清单由我们后端管理,涂鸦 SDK 仅用于设备控制。
|
* 家庭/房间/设备清单由我们后端管理,涂鸦 SDK 仅用于设备控制。
|
||||||
@ -35,6 +37,30 @@ public class BackendDevice {
|
|||||||
@SerializedName("isGateway")
|
@SerializedName("isGateway")
|
||||||
private Integer 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 String getDeviceId() { return deviceId; }
|
||||||
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
|
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
|
||||||
|
|
||||||
|
|||||||
@ -104,6 +104,9 @@ public class PlatformServiceManager {
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess() {
|
public void onSuccess() {
|
||||||
reportActivity(device, commands);
|
reportActivity(device, commands);
|
||||||
|
// 同步最新 DP 值到后端并通知首页刷新卡片状态
|
||||||
|
// (Mesh 本地通道控制时涂鸦 SDK 数据层不更新,首页会显示旧状态)
|
||||||
|
reportDps(device, commands);
|
||||||
if (callback != null) callback.onSuccess();
|
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) {
|
private void reportActivity(PlatformDevice device, Map<String, Object> commands) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1071,6 +1071,9 @@ public class DeviceControlActivity extends AppCompatActivity {
|
|||||||
public void onSuccess() {
|
public void onSuccess() {
|
||||||
// APP 走 SDK 直连不经后端,需主动上报,否则首页「控制记录」永远为空
|
// APP 走 SDK 直连不经后端,需主动上报,否则首页「控制记录」永远为空
|
||||||
reportControlActivity(cmd);
|
reportControlActivity(cmd);
|
||||||
|
// 面板操作后把最新 DP 值同步到后端并通知首页刷新卡片状态
|
||||||
|
// (Mesh 本地通道控制时涂鸦 SDK 数据层不会更新,只靠 SDK 会一直显示旧状态)
|
||||||
|
reportDpsToBackend(cmd);
|
||||||
runOnUiThread(() -> Toast.makeText(DeviceControlActivity.this,
|
runOnUiThread(() -> Toast.makeText(DeviceControlActivity.this,
|
||||||
"指令已下发", Toast.LENGTH_SHORT).show());
|
"指令已下发", 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 本地通道) */
|
/** 蓝牙 Mesh 子设备控制(走 Mesh 本地通道) */
|
||||||
private void sendSigMeshCommand(DeviceBean dev, Map<String, Object> cmd, String json) {
|
private void sendSigMeshCommand(DeviceBean dev, Map<String, Object> cmd, String json) {
|
||||||
try {
|
try {
|
||||||
@ -1155,6 +1194,8 @@ public class DeviceControlActivity extends AppCompatActivity {
|
|||||||
public void onSuccess() {
|
public void onSuccess() {
|
||||||
// APP 走 SDK 直连不经后端,需主动上报,否则首页「控制记录」永远为空
|
// APP 走 SDK 直连不经后端,需主动上报,否则首页「控制记录」永远为空
|
||||||
reportControlActivity(cmd);
|
reportControlActivity(cmd);
|
||||||
|
// 面板操作后把最新 DP 值同步到后端并通知首页刷新卡片状态
|
||||||
|
reportDpsToBackend(codeCmd);
|
||||||
runOnUiThread(() -> Toast.makeText(DeviceControlActivity.this,
|
runOnUiThread(() -> Toast.makeText(DeviceControlActivity.this,
|
||||||
"指令已下发", Toast.LENGTH_SHORT).show());
|
"指令已下发", Toast.LENGTH_SHORT).show());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,6 +96,8 @@ public class HomeFragment extends Fragment {
|
|||||||
TuyaSdkService.registerStatusCallback(deviceStatusCallback);
|
TuyaSdkService.registerStatusCallback(deviceStatusCallback);
|
||||||
// 注册动态事件回调:任意控制落库成功后,首页「近期动态」立即刷新
|
// 注册动态事件回调:任意控制落库成功后,首页「近期动态」立即刷新
|
||||||
ActivityEventBus.register(activityEventListener);
|
ActivityEventBus.register(activityEventListener);
|
||||||
|
// 注册设备状态回调:面板/中控屏操作后刷新卡片上的开关状态
|
||||||
|
ActivityEventBus.registerStateListener(deviceStateListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -104,6 +106,7 @@ public class HomeFragment extends Fragment {
|
|||||||
unregisterAllTuyaListeners();
|
unregisterAllTuyaListeners();
|
||||||
TuyaSdkService.unregisterStatusCallback(deviceStatusCallback);
|
TuyaSdkService.unregisterStatusCallback(deviceStatusCallback);
|
||||||
ActivityEventBus.unregister(activityEventListener);
|
ActivityEventBus.unregister(activityEventListener);
|
||||||
|
ActivityEventBus.unregisterStateListener(deviceStateListener);
|
||||||
if (activityRefreshTask != null) {
|
if (activityRefreshTask != null) {
|
||||||
activityRefreshHandler.removeCallbacks(activityRefreshTask);
|
activityRefreshHandler.removeCallbacks(activityRefreshTask);
|
||||||
activityRefreshTask = null;
|
activityRefreshTask = null;
|
||||||
@ -185,10 +188,11 @@ public class HomeFragment extends Fragment {
|
|||||||
*
|
*
|
||||||
* <p>用途:面板/物理开关的操作,只有在手机能收到设备 DP 回流时才会被感知
|
* <p>用途:面板/物理开关的操作,只有在手机能收到设备 DP 回流时才会被感知
|
||||||
* (此时事件总线会立即通知,不必等心跳);但当操作发生在手机不在家/无蓝牙连接时,
|
* (此时事件总线会立即通知,不必等心跳);但当操作发生在手机不在家/无蓝牙连接时,
|
||||||
* 只有后端能感知(涂鸦云消息订阅打通后)。心跳作为兜底,让首页在停留期间
|
* 只有后端能感知。心跳作为兜底,让首页在停留期间也能把后端侧新产生的动态、
|
||||||
* 也能把后端侧新产生的动态拉回来。</p>
|
* 以及其它入口(面板/中控屏)改动的设备状态拉回来。</p>
|
||||||
*
|
*
|
||||||
* <p>只刷新动态列表,不触碰设备列表,因此不会造成常用设备跳位。</p>
|
* <p>设备列表走轻量刷新(不做探活/重注册监听),且顺序固定、值没变不重绑卡片,
|
||||||
|
* 因此不会造成常用设备跳位或闪烁。</p>
|
||||||
*/
|
*/
|
||||||
private static final long ACTIVITY_HEARTBEAT_MS = 5_000L;
|
private static final long ACTIVITY_HEARTBEAT_MS = 5_000L;
|
||||||
private final android.os.Handler heartbeatHandler =
|
private final android.os.Handler heartbeatHandler =
|
||||||
@ -197,7 +201,10 @@ public class HomeFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (isAdded() && getView() != null) {
|
if (isAdded() && getView() != null) {
|
||||||
|
// 动态:后端侧产生的操控记录
|
||||||
loadRecentActivities();
|
loadRecentActivities();
|
||||||
|
// 设备状态:面板/中控屏等其它入口的操作(手机收不到 DP 回传时也能感知)
|
||||||
|
loadDevicesFromBackend(false);
|
||||||
}
|
}
|
||||||
heartbeatHandler.postDelayed(this, ACTIVITY_HEARTBEAT_MS);
|
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 */
|
/** 用户刚点击控制的设备ID */
|
||||||
@ -559,6 +575,16 @@ public class HomeFragment extends Fragment {
|
|||||||
|
|
||||||
/** 从本平台后端拉取设备列表(按当前家庭过滤) */
|
/** 从本平台后端拉取设备列表(按当前家庭过滤) */
|
||||||
private void loadDevicesFromBackend() {
|
private void loadDevicesFromBackend() {
|
||||||
|
loadDevicesFromBackend(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉取设备列表
|
||||||
|
*
|
||||||
|
* @param full true = 完整刷新(探活、监听注册、室内温度等副作用);
|
||||||
|
* false = 轻量刷新(只更新卡片状态,用于心跳等高频场景)
|
||||||
|
*/
|
||||||
|
private void loadDevicesFromBackend(boolean full) {
|
||||||
// 已成功加载过就静默刷新,避免每次 DP 同步都闪一下加载圈
|
// 已成功加载过就静默刷新,避免每次 DP 同步都闪一下加载圈
|
||||||
showLoading(!devicesLoadedOnce);
|
showLoading(!devicesLoadedOnce);
|
||||||
String auth = session.getBearerToken();
|
String auth = session.getBearerToken();
|
||||||
@ -590,10 +616,14 @@ public class HomeFragment extends Fragment {
|
|||||||
updateQuickStats();
|
updateQuickStats();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
allDevices = toTuyaDevices(list);
|
allDevices = toTuyaDevices(list, full);
|
||||||
tvEmpty.setVisibility(View.GONE);
|
tvEmpty.setVisibility(View.GONE);
|
||||||
adapter.update(allDevices);
|
adapter.update(allDevices);
|
||||||
updateQuickStats();
|
updateQuickStats();
|
||||||
|
if (!full) {
|
||||||
|
// 轻量刷新:只更新卡片状态,不做探活/监听注册等副作用
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 从涂鸦设备 DP 读取室内温湿度(温湿度传感器/空调)
|
// 从涂鸦设备 DP 读取室内温湿度(温湿度传感器/空调)
|
||||||
loadIndoorTemp();
|
loadIndoorTemp();
|
||||||
// 同步涂鸦默认家庭 gid 给 BizBundle(原生面板依赖 gid)
|
// 同步涂鸦默认家庭 gid 给 BizBundle(原生面板依赖 gid)
|
||||||
@ -1206,7 +1236,7 @@ public class HomeFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 后端设备 → TuyaDevice(复用现有卡片 UI) */
|
/** 后端设备 → TuyaDevice(复用现有卡片 UI) */
|
||||||
private List<TuyaDevice> toTuyaDevices(List<BackendDevice> list) {
|
private List<TuyaDevice> toTuyaDevices(List<BackendDevice> list, boolean allowCorrection) {
|
||||||
sdkDeviceDataMissing = false;
|
sdkDeviceDataMissing = false;
|
||||||
List<TuyaDevice> result = new ArrayList<>();
|
List<TuyaDevice> result = new ArrayList<>();
|
||||||
for (BackendDevice bd : list) {
|
for (BackendDevice bd : list) {
|
||||||
@ -1235,14 +1265,14 @@ public class HomeFragment extends Fragment {
|
|||||||
bd.getDeviceId(), devBean, bd.isOnline(), bd.getParentDeviceId());
|
bd.getDeviceId(), devBean, bd.isOnline(), bd.getParentDeviceId());
|
||||||
td.setOnline(sdkOnline);
|
td.setOnline(sdkOnline);
|
||||||
// 与后端不一致时回写纠正,保证下次进入/其他页面也准确
|
// 与后端不一致时回写纠正,保证下次进入/其他页面也准确
|
||||||
if (sdkOnline != bd.isOnline()) {
|
if (sdkOnline != bd.isOnline() && allowCorrection) {
|
||||||
android.util.Log.i("HomeFragment", "在线状态纠正: " + bd.getDeviceName()
|
android.util.Log.i("HomeFragment", "在线状态纠正: " + bd.getDeviceName()
|
||||||
+ " 后端=" + bd.getStatus() + " SDK=" + sdkOnline);
|
+ " 后端=" + bd.getStatus() + " SDK=" + sdkOnline);
|
||||||
reportStatusToBackend(bd.getDeviceId(), sdkOnline, null);
|
reportStatusToBackend(bd.getDeviceId(), sdkOnline, null);
|
||||||
}
|
}
|
||||||
if (devBean != null) {
|
if (devBean != null) {
|
||||||
if (devBean.getDps() != null) {
|
if (devBean.getDps() != null) {
|
||||||
td.setDps(devBean.getDps());
|
td.setDps(new HashMap<>(devBean.getDps()));
|
||||||
}
|
}
|
||||||
if (devBean.getCategory() != null && !devBean.getCategory().isEmpty()) {
|
if (devBean.getCategory() != null && !devBean.getCategory().isEmpty()) {
|
||||||
td.setCategory(devBean.getCategory());
|
td.setCategory(devBean.getCategory());
|
||||||
@ -1259,6 +1289,11 @@ public class HomeFragment extends Fragment {
|
|||||||
android.util.Log.w("HomeFragment", "SDK状态读取异常 " + bd.getDeviceName(), e);
|
android.util.Log.w("HomeFragment", "SDK状态读取异常 " + bd.getDeviceName(), e);
|
||||||
}
|
}
|
||||||
result.add(td);
|
result.add(td);
|
||||||
|
// DP 值合并:以后端为准。
|
||||||
|
// 后端是多来源汇聚点(APP 控制回传 / 面板·中控屏上报 / 涂鸦云消息订阅都会写入),
|
||||||
|
// 而涂鸦 SDK 数据层在「Mesh 本地通道控制」「子设备无云端推送」时不会更新,
|
||||||
|
// 只用 SDK 的 dps 会出现「面板已打开,首页仍显示已关闭」。
|
||||||
|
mergeBackendDps(bd, td);
|
||||||
}
|
}
|
||||||
// 1) 先按「上一次显示的顺序」重排:设备位置对用户是固定的,
|
// 1) 先按「上一次显示的顺序」重排:设备位置对用户是固定的,
|
||||||
// 不能因为一次控制上报就让常用设备换位置(JD2BTSA 跳到 JD6BTSA 前面)。
|
// 不能因为一次控制上报就让常用设备换位置(JD2BTSA 跳到 JD6BTSA 前面)。
|
||||||
@ -1272,6 +1307,45 @@ public class HomeFragment extends Fragment {
|
|||||||
/** 上一次渲染的设备顺序(设备ID),用于保持列表位置稳定 */
|
/** 上一次渲染的设备顺序(设备ID),用于保持列表位置稳定 */
|
||||||
private final List<String> lastDeviceOrder = new ArrayList<>();
|
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 {
|
} else {
|
||||||
deviceStateMapper.updateById(state);
|
deviceStateMapper.updateById(state);
|
||||||
}
|
}
|
||||||
|
// 同步到设备能力(dps):中控屏/面板改动的设备状态要让 APP 卡片读到,
|
||||||
|
// 否则 APP 只能看到涂鸦 SDK 里的旧值(面板已打开、首页仍显示已关闭)。
|
||||||
|
syncCapabilitiesDps(deviceId, propertyCode, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
PanelReportDto.PropertyResult result = new PanelReportDto.PropertyResult();
|
PanelReportDto.PropertyResult result = new PanelReportDto.PropertyResult();
|
||||||
@ -126,6 +129,43 @@ public class PanelStateService {
|
|||||||
return result;
|
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