fix(U): 修复远程(不在网关附近)打开APP时子设备显示离线且无法控制
- 在线判定: Mesh/Zigbee 子设备没有独立在线通道, 涂鸦云 isOnline 恒 false, 远程时无本地探活缓存 → 只显示网关在线。改为以「父网关在线」为判定依据 (后端同样按父网关聚合), 新增 probeOffline 标记支持反向证据 - 控制通道: 蓝牙 Mesh 网关可达时走本地通道, 不可达(远程)时自动改走涂鸦云端下发; 面板控制同步改造, 本地失败自动回退云端 - 失败提示: Mesh 子设备远程不可控时给出明确指引(现场蓝牙/面板), 不再笼统报控制失败
This commit is contained in:
parent
18686dba7c
commit
7e7c1e072c
@ -132,12 +132,34 @@ public class TuyaPlatformService implements IPlatformService {
|
||||
ControlCallback callback) {
|
||||
try {
|
||||
DeviceBean dev = ThingHomeSdk.getDataInstance().getDeviceBean(deviceId);
|
||||
// 蓝牙 Mesh 子设备:走 Mesh 本地控制通道(云端 publishDps 对离线 Mesh 子设备会失败)
|
||||
// 蓝牙 Mesh 子设备:
|
||||
// - 手机在网关附近(蓝牙可达)→ 走 Mesh 本地通道,最快最可靠
|
||||
// - 远程(不在同一空间/局域网)→ 蓝牙不可达,必须改走涂鸦云端下发
|
||||
if (dev != null && dev.isSigMesh()) {
|
||||
controlSigMeshDevice(dev, commands, callback);
|
||||
if (TuyaSdkService.isMeshReachable()) {
|
||||
controlSigMeshDevice(dev, commands, callback, true);
|
||||
} else {
|
||||
Log.i(TAG, "Mesh网关不可达, 改走云端下发 devId=" + deviceId);
|
||||
controlByCloud(dev, commands, callback, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 普通设备:云端 publishDps(统一把 DP 数字 id 转换为涂鸦 code)
|
||||
controlByCloud(dev, commands, callback, false);
|
||||
} catch (Exception e) {
|
||||
if (callback != null) callback.onError("ex", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 云端下发指令。
|
||||
*
|
||||
* @param fallbackToMesh 云端失败后是否回退到 Mesh 本地通道
|
||||
*/
|
||||
private void controlByCloud(DeviceBean dev, Map<String, Object> commands,
|
||||
ControlCallback callback, boolean fallbackToMesh) {
|
||||
try {
|
||||
String deviceId = dev != null ? dev.getDevId() : "";
|
||||
IThingDevice device = ThingHomeSdk.newDeviceInstance(deviceId);
|
||||
Map<String, Object> codeCommands =
|
||||
TuyaSdkService.convertDpIdToCode(deviceId, commands);
|
||||
@ -150,6 +172,13 @@ public class TuyaPlatformService implements IPlatformService {
|
||||
|
||||
@Override
|
||||
public void onError(String code, String error) {
|
||||
Log.w(TAG, "云端下发失败 devId=" + deviceId + " code=" + code
|
||||
+ " err=" + error);
|
||||
if (fallbackToMesh && dev != null && dev.isSigMesh()) {
|
||||
// 蓝牙可达但云端失败(如设备仅本地在线)→ 回退 Mesh 本地通道
|
||||
controlSigMeshDevice(dev, commands, callback, false);
|
||||
return;
|
||||
}
|
||||
if (callback != null) callback.onError(code, error);
|
||||
}
|
||||
});
|
||||
@ -158,9 +187,13 @@ public class TuyaPlatformService implements IPlatformService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 控制蓝牙 Mesh 子设备(走 Mesh 本地通道) */
|
||||
/**
|
||||
* 控制蓝牙 Mesh 子设备(走 Mesh 本地通道)。
|
||||
*
|
||||
* @param fallbackToCloud 本地通道失败后是否回退云端(远程时蓝牙不可达)
|
||||
*/
|
||||
private void controlSigMeshDevice(DeviceBean dev, Map<String, Object> commands,
|
||||
ControlCallback callback) {
|
||||
ControlCallback callback, boolean fallbackToCloud) {
|
||||
try {
|
||||
String meshId = dev.getMeshId();
|
||||
String nodeId = dev.getNodeId();
|
||||
@ -183,6 +216,12 @@ public class TuyaPlatformService implements IPlatformService {
|
||||
public void onError(String code, String error) {
|
||||
Log.e(TAG, "Mesh控制失败 nodeId=" + nodeId + " code=" + code
|
||||
+ " err=" + error);
|
||||
if (fallbackToCloud) {
|
||||
// 蓝牙通道失败 → 回退涂鸦云端(远程场景必须走云端)
|
||||
Log.i(TAG, "Mesh本地控制失败, 回退云端 devId=" + dev.getDevId());
|
||||
controlByCloud(dev, commands, callback, false);
|
||||
return;
|
||||
}
|
||||
if (callback != null) callback.onError(code, error);
|
||||
}
|
||||
});
|
||||
|
||||
@ -368,30 +368,46 @@ public class TuyaSdkService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合判断涂鸦设备的在线状态。
|
||||
* 综合判断涂鸦设备的在线状态(兼容旧调用:不传父网关在线状态)。
|
||||
*
|
||||
* 核心结论(实测数据得出):
|
||||
* 1. **不能用「父网关在线 ⇒ 子设备在线」推断。**
|
||||
* 网关 301网关 IsOnline=true,但其下 JD6BTSA 的 IsOnline=false —— 网关在线,
|
||||
* 插座断电时子设备依然是离线。涂鸦云端会独立维护每个设备的在线状态。
|
||||
* 2. **在线判定只信设备自己的 IsOnline()/IsLocalOnline()。**
|
||||
* 实测值:301网关 IsOnline=true;DMD3CC/窗帘开关/JD6BTSA IsOnline=false,
|
||||
* 与涂鸦 App 显示一致。
|
||||
* 3. IsLocalOnline 只表示「手机和该设备在同一局域网可直连」,本项目手机与设备
|
||||
* 不在同一网络(设备 Ip 是公网),所以它恒为 false,**不能**当成离线依据;
|
||||
* 但为 true 时说明设备就在身边,也算在线。
|
||||
* 4. 数据层未同步时 IsOnline 可能是 null(不是 false),此时才回退后端 status。
|
||||
*
|
||||
* @param parentDevId 仅用于日志标识父子关系,不参与在线判定
|
||||
* @see #resolveDeviceOnline(String, DeviceBean, boolean, String, boolean)
|
||||
*/
|
||||
public static boolean resolveDeviceOnline(String devId,
|
||||
com.thingclips.smart.sdk.bean.DeviceBean bean, boolean backendOnline,
|
||||
String parentDevId) {
|
||||
return resolveDeviceOnline(devId, bean, backendOnline, parentDevId, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合判断涂鸦设备的在线状态。
|
||||
*
|
||||
* <p>核心结论(实测数据得出):</p>
|
||||
* <ol>
|
||||
* <li><b>子设备(SigMesh/Zigbee)没有独立的在线通道</b>:涂鸦云端对它们的
|
||||
* isOnline 恒为 false(实测 JD2BTSA/JD6BTSA 通电且网关在线,云端仍 false)。
|
||||
* 局域网场景下靠"探活确认"缓存能撑 10 分钟,一旦过期或远程打开 APP,
|
||||
* 就会退回"离线" —— 这正是远程打开 APP 只看到网关在线的原因。
|
||||
* 因此子设备一律以「父网关在线」为判定依据。</li>
|
||||
* <li>主设备/网关:云端或局域网任一明确在线即在线;云端明确 false 则离线。</li>
|
||||
* <li>探活结果优先级最高:确认在线 → 在线;确认离线 → 离线
|
||||
* (这样插座真断电时仍能识别出来)。</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param parentOnline 父网关当前是否在线(子设备判定用)
|
||||
*/
|
||||
public static boolean resolveDeviceOnline(String devId,
|
||||
com.thingclips.smart.sdk.bean.DeviceBean bean, boolean backendOnline,
|
||||
String parentDevId, boolean parentOnline) {
|
||||
// 探活确认期内,设备的真实在线状态已经过通信验证,云端快照不可信
|
||||
if (isProbeOnline(devId)) {
|
||||
Log.i(TAG, "在线判断[" + devId + "] 探活确认期内 → 在线");
|
||||
return true;
|
||||
}
|
||||
// 探活明确失败 → 判离线(覆盖父网关在线的推断,识别真实断电)
|
||||
if (isProbeOffline(devId)) {
|
||||
Log.i(TAG, "在线判断[" + devId + "] 探活确认离线 → 离线");
|
||||
return false;
|
||||
}
|
||||
if (bean == null) {
|
||||
// getDeviceBean() 在家庭详情未同步时恒为 null,回退到 queryHomeList 的设备缓存
|
||||
bean = sdkDeviceCache.get(devId);
|
||||
@ -412,12 +428,28 @@ public class TuyaSdkService {
|
||||
Object v = m.invoke(bean);
|
||||
if (v instanceof Number) activeTime = ((Number) v).longValue();
|
||||
} catch (Throwable ignored) {}
|
||||
|
||||
// 是否子设备:后端父子关系 或 SDK 的 parentId
|
||||
boolean isSub = (parentDevId != null && !parentDevId.isEmpty() && !parentDevId.equals(devId))
|
||||
|| isTrueSubDevice(bean);
|
||||
|
||||
boolean online;
|
||||
if (Boolean.TRUE.equals(cloud) || Boolean.TRUE.equals(local)) {
|
||||
if (isSub) {
|
||||
// 子设备:云端 isOnline 恒 false,不能采信。
|
||||
// 以父网关在线为判定依据;父网关离线才判离线。
|
||||
if (parentOnline) {
|
||||
online = true;
|
||||
} else if (Boolean.TRUE.equals(cloud) || Boolean.TRUE.equals(local)) {
|
||||
online = true;
|
||||
} else {
|
||||
// 父网关不在线 → 采信后端结论(后端同样按父网关聚合过)
|
||||
online = backendOnline;
|
||||
}
|
||||
} else if (Boolean.TRUE.equals(cloud) || Boolean.TRUE.equals(local)) {
|
||||
// 云端或局域网任一明确在线 → 在线
|
||||
online = true;
|
||||
} else if (cloud != null) {
|
||||
// 云端明确返回 false → 设备确实离线(子设备断电 / 网关掉线都会是这个值)
|
||||
// 云端明确返回 false → 设备确实离线(主设备断电 / 网关掉线)
|
||||
online = false;
|
||||
} else if (Boolean.FALSE.equals(local)) {
|
||||
// 云端未知但局域网明确 false:不能据此说离线(手机可能不在同一网络),
|
||||
@ -428,9 +460,10 @@ public class TuyaSdkService {
|
||||
online = backendOnline;
|
||||
}
|
||||
Log.i(TAG, "在线判断[" + devId + "/" + bean.getName() + "]"
|
||||
+ (isTrueSubDevice(bean) ? "[子设备]" : "[主/网关设备]")
|
||||
+ (isSub ? "[子设备]" : "[主/网关设备]")
|
||||
+ " cloud=" + cloud + " local=" + local + " activeTime=" + activeTime
|
||||
+ " backend=" + backendOnline + " → " + online);
|
||||
+ " backend=" + backendOnline + " parentOnline=" + parentOnline
|
||||
+ " → " + online);
|
||||
return online;
|
||||
}
|
||||
|
||||
@ -463,6 +496,72 @@ public class TuyaSdkService {
|
||||
* 因此:探活(下发读属性查询并收到设备上报)确认在线后,在 TTL 内一律视为在线,
|
||||
* 云端快照/离线事件在确认期内被忽略。
|
||||
*/
|
||||
/**
|
||||
* 探活确认离线的设备(devId → 确认时间戳 ms):用于识别真实断电。
|
||||
*
|
||||
* <p>子设备按父网关在线判定在线后,就失去了识别「插座真断电」的能力;
|
||||
* 这里用探活失败作为反向证据,短时间内判离线。</p>
|
||||
*/
|
||||
private static final java.util.concurrent.ConcurrentHashMap<String, Long> offlineOverride =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
/** 探活确认离线的有效期(毫秒):比在线确认期短,避免一次误判长期离线 */
|
||||
private static final long OFFLINE_OVERRIDE_TTL_MS = 3 * 60 * 1000L;
|
||||
|
||||
/** 标记探活确认离线 */
|
||||
public static void markProbeOffline(String devId) {
|
||||
if (devId != null && !devId.isEmpty()) {
|
||||
offlineOverride.put(devId, System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
/** 是否处于探活确认离线期内 */
|
||||
public static boolean isProbeOffline(String devId) {
|
||||
if (devId == null) return false;
|
||||
Long t = offlineOverride.get(devId);
|
||||
if (t == null) return false;
|
||||
if (System.currentTimeMillis() - t > OFFLINE_OVERRIDE_TTL_MS) {
|
||||
offlineOverride.remove(devId);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 最近一次 Mesh(蓝牙网关)本地连接成功的时间。
|
||||
*
|
||||
* <p>用于判断手机是否还在网关附近:Mesh 子设备只能通过蓝牙本地通道控制,
|
||||
* 不在附近时必须改走云端,否则远程操作必然失败。</p>
|
||||
*/
|
||||
private static volatile long lastMeshConnectedAt = 0L;
|
||||
|
||||
/** 标记 Mesh 网关本地连接成功 */
|
||||
public static void markMeshConnected() {
|
||||
lastMeshConnectedAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/** 蓝牙 Mesh 网关是否在可达范围内(10 分钟内有成功连接) */
|
||||
public static boolean isMeshReachable() {
|
||||
if (lastMeshConnectedAt <= 0) return false;
|
||||
return System.currentTimeMillis() - lastMeshConnectedAt < 10 * 60 * 1000L;
|
||||
}
|
||||
|
||||
/** 是否是蓝牙 Mesh 子设备(只能通过网关附近蓝牙控制) */
|
||||
public static boolean isSigMeshDevice(String deviceId) {
|
||||
if (deviceId == null) return false;
|
||||
try {
|
||||
com.thingclips.smart.sdk.bean.DeviceBean bean =
|
||||
com.thingclips.smart.home.sdk.ThingHomeSdk.getDataInstance()
|
||||
.getDeviceBean(deviceId);
|
||||
if (bean == null) {
|
||||
bean = sdkDeviceCache.get(deviceId);
|
||||
}
|
||||
return bean != null && bean.isSigMesh();
|
||||
} catch (Throwable e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final java.util.concurrent.ConcurrentHashMap<String, Long> onlineOverride =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
@ -1472,6 +1571,11 @@ public class TuyaSdkService {
|
||||
public void onConnectStatusChanged(String id, int status) {
|
||||
Log.i(TAG, "SIG Mesh 连接状态变更 meshId=" + id
|
||||
+ " status=" + status);
|
||||
// status: 0=已连接, 1=未连接, 2=连接中(涂鸦 SDK 定义)
|
||||
if (status == 0) {
|
||||
// 蓝牙网关可达 → Mesh 子设备可用本地通道控制
|
||||
markMeshConnected();
|
||||
}
|
||||
// 连接成功时通知 UI 刷新(子设备可能已识别为在线)
|
||||
notifyStatusChanged("", true);
|
||||
}
|
||||
|
||||
@ -1213,6 +1213,13 @@ public class DeviceControlActivity extends AppCompatActivity {
|
||||
android.util.Log.i("DeviceControlActivity",
|
||||
"Mesh子设备控制 meshId=" + meshId + " nodeId=" + nodeId
|
||||
+ " category=" + category + " cmd=" + codeCmd);
|
||||
// 远程场景(不在网关附近):蓝牙通道不可达,直接走涂鸦云端
|
||||
if (!TuyaSdkService.isMeshReachable()) {
|
||||
android.util.Log.i("DeviceControlActivity",
|
||||
"蓝牙网关不可达, 直接走云端下发 device=" + deviceId);
|
||||
sendCloudCommand(dev, cmd, codeCmd);
|
||||
return;
|
||||
}
|
||||
IThingBlueMeshDevice meshDev = ThingOSMesh.newDevice(meshId);
|
||||
meshDev.publishCommands(nodeId, category, codeCmd, new IResultCallback() {
|
||||
@Override
|
||||
@ -1230,10 +1237,31 @@ public class DeviceControlActivity extends AppCompatActivity {
|
||||
"Mesh控制失败 device=" + deviceId + " code=" + code
|
||||
+ " error=" + error + ",回退云端 publishDps");
|
||||
// Mesh 本地通道失败(设备未连蓝牙/Mesh 离线,或被误判为 Mesh):回退云端控制
|
||||
sendCloudCommand(dev, cmd, codeCmd);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("DeviceControlActivity", "Mesh控制异常: " + e.getMessage(), e);
|
||||
runOnUiThread(() -> Toast.makeText(DeviceControlActivity.this,
|
||||
"控制失败: " + e.getMessage(), Toast.LENGTH_LONG).show());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 涂鸦云端下发指令(远程控制路径)。
|
||||
*
|
||||
* <p>Mesh 子设备若涂鸦云不支持经网关透传,这里会失败,此时给出明确提示
|
||||
* (该设备是蓝牙 Mesh 子设备,需在现场控制),而不是笼统的"控制失败"。</p>
|
||||
*/
|
||||
private void sendCloudCommand(DeviceBean device, Map<String, Object> cmd,
|
||||
Map<String, Object> codeCmd) {
|
||||
try {
|
||||
device.publishDps(JSONObject.toJSONString(codeCmd),
|
||||
new IResultCallback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
reportControlActivity(cmd);
|
||||
reportDpsToBackend(codeCmd);
|
||||
runOnUiThread(() -> Toast.makeText(
|
||||
DeviceControlActivity.this,
|
||||
"指令已下发", Toast.LENGTH_SHORT).show());
|
||||
@ -1243,18 +1271,17 @@ public class DeviceControlActivity extends AppCompatActivity {
|
||||
public void onError(String c2, String e2) {
|
||||
android.util.Log.e("DeviceControlActivity",
|
||||
"云端控制失败 device=" + deviceId
|
||||
+ " code=" + c2 + " error=" + e2
|
||||
+ " cmd=" + json);
|
||||
+ " code=" + c2 + " error=" + e2);
|
||||
boolean mesh = device != null && device.isSigMesh();
|
||||
String msg = mesh
|
||||
? "该设备是蓝牙 Mesh 子设备,远程无法直接控制。\n"
|
||||
+ "请到网关附近用手机蓝牙控制,或使用现场面板。"
|
||||
: "控制失败: " + (e2 != null ? e2 : c2);
|
||||
runOnUiThread(() -> Toast.makeText(
|
||||
DeviceControlActivity.this,
|
||||
"控制失败: " + (e2 != null ? e2 : c2),
|
||||
Toast.LENGTH_LONG).show());
|
||||
}
|
||||
});
|
||||
DeviceControlActivity.this, msg, Toast.LENGTH_LONG).show());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("DeviceControlActivity", "Mesh控制异常: " + e.getMessage(), e);
|
||||
runOnUiThread(() -> Toast.makeText(DeviceControlActivity.this,
|
||||
"控制失败: " + e.getMessage(), Toast.LENGTH_LONG).show());
|
||||
}
|
||||
|
||||
@ -1255,8 +1255,33 @@ public class HomeFragment extends Fragment {
|
||||
private List<TuyaDevice> toTuyaDevices(List<BackendDevice> list, boolean allowCorrection) {
|
||||
sdkDeviceDataMissing = false;
|
||||
List<TuyaDevice> result = new ArrayList<>();
|
||||
// 父设备在线映射(网关):子设备在线判定依赖它
|
||||
Map<String, Boolean> parentOnlineMap = new HashMap<>();
|
||||
for (BackendDevice d : list) {
|
||||
if (d.getDeviceId() == null) continue;
|
||||
boolean isParent = (d.getIsGateway() != null && d.getIsGateway() == 1);
|
||||
if (!isParent) {
|
||||
for (BackendDevice c : list) {
|
||||
if (d.getDeviceId().equals(c.getParentDeviceId())) {
|
||||
isParent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isParent) {
|
||||
parentOnlineMap.put(d.getDeviceId(), d.isOnline());
|
||||
}
|
||||
}
|
||||
for (BackendDevice bd : list) {
|
||||
TuyaDevice td = new TuyaDevice();
|
||||
// 父网关在线映射:Mesh/Zigbee 子设备没有独立在线通道,
|
||||
// 涂鸦云端对它们的 isOnline 恒为 false,远程打开 APP 时会误判离线,
|
||||
// 因此子设备一律以「父网关在线」为判定依据。
|
||||
boolean parentOnline = false;
|
||||
if (bd.getParentDeviceId() != null && !bd.getParentDeviceId().isEmpty()) {
|
||||
Boolean p = parentOnlineMap.get(bd.getParentDeviceId());
|
||||
parentOnline = Boolean.TRUE.equals(p);
|
||||
}
|
||||
td.setId(bd.getDeviceId());
|
||||
td.setName(bd.getDeviceName());
|
||||
td.setOnline(bd.isOnline());
|
||||
@ -1278,7 +1303,8 @@ public class HomeFragment extends Fragment {
|
||||
// 子设备用自身的 getIsOnline()/getIsLocalOnline(),
|
||||
// 不能因为父网关在线就推断子设备在线(子设备可能断电)。
|
||||
boolean sdkOnline = TuyaSdkService.resolveDeviceOnline(
|
||||
bd.getDeviceId(), devBean, bd.isOnline(), bd.getParentDeviceId());
|
||||
bd.getDeviceId(), devBean, bd.isOnline(), bd.getParentDeviceId(),
|
||||
parentOnline);
|
||||
td.setOnline(sdkOnline);
|
||||
// 与后端不一致时回写纠正,保证下次进入/其他页面也准确
|
||||
if (sdkOnline != bd.isOnline() && allowCorrection) {
|
||||
|
||||
@ -254,10 +254,18 @@ public class HomeDeviceAdapter extends RecyclerView.Adapter<HomeDeviceAdapter.Vi
|
||||
public void onError(String code, String error) {
|
||||
unlockPendingToggle(device.getId());
|
||||
holder.pill.post(() -> holder.pill.setChecked(!checked));
|
||||
// 蓝牙 Mesh 子设备远程不可控:给出可操作的提示
|
||||
String msg;
|
||||
if (TuyaSdkService.isSigMeshDevice(device.getId())
|
||||
&& !TuyaSdkService.isMeshReachable()) {
|
||||
msg = "该设备是蓝牙 Mesh 子设备,远程无法直接控制。\n"
|
||||
+ "请到网关附近用手机蓝牙控制,或使用现场面板。";
|
||||
} else {
|
||||
msg = "控制失败: " + (error != null ? error : code);
|
||||
}
|
||||
android.widget.Toast.makeText(
|
||||
holder.itemView.getContext(),
|
||||
"控制失败: " + (error != null ? error : code),
|
||||
android.widget.Toast.LENGTH_SHORT).show();
|
||||
holder.itemView.getContext(), msg,
|
||||
android.widget.Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
holder.tvStatus.setText(buildStatusText(holder.tvStatus, true, true, checked, false));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user