背景信息

  • 需求描述:

    灯泡默认显示白色光,平时打开灯泡,显示黄色,遇到节假日,显示蓝色。

  • 分析:

    由于灯泡没有存储能力,关灯之后不会存储自己上一次显示的状态,使用设备影子,能够实现这一功能。灯泡打开时,去读取影子信息,显示对应的状态,然后上报到云端,存储当前状态。之后每次打开,都去读取影子信息,显示期望改变成的状态或上一次显示的状态。

操作步骤

  1. 登录物联网平台的控制台,设置产品和设备。
  2. 设备影子demo,填写相关信息。
    
    /**
    * 设备key和secret信息
    */
    private static String deviceName = "********";
    private static String productKey = "******";
    private static String deviceSecret = "**************";
  3. 灯泡开灯时,主动获取影子信息。
    • 第一次开灯时,没有影子信息,灯泡显示默认状态,并上报信息到影子中。
      
      JSONObject errorJsonObj = payloadJsonObj.getJSONObject("content");
      String errorCode = errorJsonObj.getString("errorcode");
      String errorMsg = errorJsonObj.getString("errormessage");
      //如果是影子没有内容,上报本次灯泡状态
      if (RESULT_CODE_NO_SHADOW.equals(errorCode)){
      System.out.println("影子是空的,显示白色,上报当前灯泡状态。");
      //更新设备影子
      Map attMap = new HashMap(128);
      attMap.put("color", "white");
      String shadowUpdateMsg = genUpdateShadowMsg(attMap);
      System.out.println("updateShadowMsg: " + shadowUpdateMsg);
      MqttMessage shadowMessage = new MqttMessage(shadowUpdateMsg.getBytes("UTF-8"));
      message.setQos(1);
      sampleClient.publish(shadowUpdateTopic, shadowMessage);
      }else {
      System.out.println("errorCode:" + errorCode);
      System.out.println("errorMsg:" + errorMsg);
      }
    • 之后开灯时,获取影子信息,如果有desired的color,按照color显示,同时将该颜色上报到影子,并清空desired信息;如果没有期望的颜色,获取上一次上报的color显示。
      
      JSONObject shadowJsonObj = JSON.parseObject(message.toString());
      JSONObject payloadJsonObj = shadowJsonObj.getJSONObject("payload");
      shadowVersion = shadowJsonObj.getLong("version");
      LogUtil.print("shadowVersion:" + shadowVersion);
      //解析出desired和reported信息
      JSONObject stateJsonObj = payloadJsonObj.getJSONObject("state");
      String desiredString = stateJsonObj.getString("desired");
      String reportedString = stateJsonObj.getString("reported");
      LogUtil.print("desiredString:" + desiredString);
      if (desiredString != null) {
      //根据desired信息做业务处理
      String color = JSON.parseObject(desiredString).getString("color");
      System.out.println("要换颜色喽,灯泡显示颜色:" + color);
      //更新color信息到reported中
      if (color != null){
      String updateShadowReportdMsg = genUpdateShadowReportdMsg(reportedString, color);
      LogUtil.print("updateShadowReportdMsg:" + updateShadowReportdMsg);
      MqttMessage cleanShadowMqttMsg = new MqttMessage(updateShadowReportdMsg.getBytes("UTF-8"));
      message.setQos(1);
      sampleClient.publish(shadowUpdateTopic, cleanShadowMqttMsg);
      LogUtil.print("shadow reported msg update success");
      }
      //清空desired信息
      String cleanShadowMsg = genCleanShadowMsg(reportedString);
      LogUtil.print("cleanShadowMsg:" + cleanShadowMsg);
      MqttMessage cleanShadowMqttMsg = new MqttMessage(cleanShadowMsg.getBytes("UTF-8"));
      message.setQos(1);
      sampleClient.publish(shadowUpdateTopic, cleanShadowMqttMsg);
      LogUtil.print("send clean shadow msg done");
      }else {
      //没有desired信息 灯泡显示reported中color状态 
      if (reportedString != null){
      System.out.println("开灯了,灯泡显示上一次颜色:" + JSON.parseObject(reportedString).getString("color"));
      }
      }
  4. 验证是否设置成功。
    • 首次开灯,第一次运行。


    • 关灯后,服务端修改灯泡期望颜色,灯泡开灯。




    • 关灯后再开


    如果要想改变灯泡的显示颜色,只需修改影子信息即可,没有存储能力的灯泡,通过设备影子实现了偏好设置。

操作样例

完整demo代码


/**设备端查询影子method*/
private static final String SHADOW_METHOD_REPLY = "reply";
/**服务端下发method*/
private static final String SHADOW_METHOD_CONTROL = "control";
private static final String AUTH_RESULT_SUCCESS_KEY = "code";
private static final String RESULT_CODE_SUCCESS = "200";
private static final String RESULT_CODE_NO_SHADOW = "407";
private static final String RESULT_STATUS_SUCCESS = "success";
/**
* 认证服务器地址 每个区域不一样
*/
private static String authUrl = "https://iot-auth.cn-shanghai.aliyuncs.com/auth/devicename";
/**
* 设备key和secret信息
*/
private static String deviceName = "***";
private static String productKey = "***";
private static String deviceSecret = "***";
/**
* 设备影子topic
*/
private static String shadowAckTopic = "/shadow/get/" + productKey + "/" + deviceName;
private static String shadowUpdateTopic = "/shadow/update/" + productKey + "/" + deviceName;
/**
* 影子版本号
*/
private static long shadowVersion = 0;
/**
* 根据属性key-value 生成shadow json格式数据
*
* @param attributeMap
* @return
*/
private static String genUpdateShadowMsg(Map attributeMap) {
Set attSet = attributeMap.keySet();
Map attMap = new LinkedHashMap();
for (String attKey : attSet) {
attMap.put(attKey, attributeMap.get(attKey));
}
Map reportedMap = new LinkedHashMap();
reportedMap.put("reported", attMap);
Map shadowJsonMap = new LinkedHashMap();
shadowJsonMap.put("method", "update");
shadowJsonMap.put("state", reportedMap);
//shadow version自增
shadowVersion++;
shadowJsonMap.put("version", shadowVersion);
return JSON.toJSONString(shadowJsonMap);
}
/**
* 生成clean shadow json数据
*
* @param reportMsg
* @return
*/
private static String genCleanShadowMsg(String reportMsg) {
Map stateMap = new LinkedHashMap();
if (reportMsg == null || reportMsg.length() == 0) {
stateMap.put("reported", "null");
} else {
JSONObject reportJsonObj = JSON.parseObject(reportMsg);
Set attSet = reportJsonObj.keySet();
Map attMap = new LinkedHashMap();
for (String attKey : attSet) {
attMap.put(attKey, reportJsonObj.getString(attKey));
}
stateMap.put("reported", attMap);
}
stateMap.put("desired", "null");
Map cleanShadowMap = new LinkedHashMap();
cleanShadowMap.put("method", "update");
cleanShadowMap.put("state", stateMap);
shadowVersion++;
cleanShadowMap.put("version", shadowVersion);
return JSON.toJSONString(cleanShadowMap);
}
/**
* 更新影子reported json数据 ,将color同步更新到reported中
*
* @param reportMsg
* @param color 
* @return
*/
private static String genUpdateShadowReportdMsg(String reportMsg, String color) {
Map stateMap = new LinkedHashMap();
Map attMap = new LinkedHashMap();
if (reportMsg != null){
JSONObject reportJsonObj = JSON.parseObject(reportMsg);
Set attSet = reportJsonObj.keySet();
for (String attKey : attSet) {
attMap.put(attKey, reportJsonObj.getString(attKey));
}
}
attMap.put("color", color);
stateMap.put("reported", attMap);
Map cleanShadowMap = new LinkedHashMap();
cleanShadowMap.put("method", "update");
cleanShadowMap.put("state", stateMap);
shadowVersion++;
cleanShadowMap.put("version", shadowVersion);
return JSON.toJSONString(cleanShadowMap);
}
/**
* 解析出desired信息
*
* @param message
* @param sampleClient
* @throws Exception
*/
private static void parseDesiredMsg(MqttMessage message, MqttClient sampleClient) throws Exception {
JSONObject shadowJsonObj = JSON.parseObject(message.toString());
JSONObject payloadJsonObj = shadowJsonObj.getJSONObject("payload");
shadowVersion = shadowJsonObj.getLong("version");
LogUtil.print("shadowVersion:" + shadowVersion);
//解析出desired
JSONObject stateJsonObj = payloadJsonObj.getJSONObject("state");
String desiredString = stateJsonObj.getString("desired");
String reportedString = stateJsonObj.getString("reported");
LogUtil.print("desiredString:" + desiredString);
//清空shadow信息
if (desiredString != null) {
//TODO 根据desired信息做业务处理
String color = JSON.parseObject(desiredString).getString("color");
System.out.println("要换颜色喽,灯泡显示颜色:" + color);
//更新color信息到reported中
if (color != null){
String updateShadowReportdMsg = genUpdateShadowReportdMsg(reportedString, color);
LogUtil.print("updateShadowReportdMsg:" + updateShadowReportdMsg);
MqttMessage cleanShadowMqttMsg = new MqttMessage(updateShadowReportdMsg.getBytes("UTF-8"));
message.setQos(1);
sampleClient.publish(shadowUpdateTopic, cleanShadowMqttMsg);
LogUtil.print("shadow reported msg update success");
}
//清空desired信息
String cleanShadowMsg = genCleanShadowMsg(reportedString);
LogUtil.print("cleanShadowMsg:" + cleanShadowMsg);
MqttMessage cleanShadowMqttMsg = new MqttMessage(cleanShadowMsg.getBytes("UTF-8"));
message.setQos(1);
sampleClient.publish(shadowUpdateTopic, cleanShadowMqttMsg);
LogUtil.print("send clean shadow msg done");
}else {
//没有desired信息 灯泡显示reported状态 
if (reportedString != null){
System.out.println("开灯了,灯泡显示上一次颜色:" + JSON.parseObject(reportedString).getString("color"));
}
}
}
public static void main(String... strings) throws Exception {
/* 客户端设备 自己的一个标记 */
String clientId = productKey + "&" + deviceName;
Map params = new HashMap(16);
/** 这个是对应用户在控制台注册的 设备productkey */
params.put("productKey", productKey);
/** 这个是对应用户在控制台注册的 设备name */
params.put("deviceName", deviceName);
params.put("timestamp", "" + System.currentTimeMillis());
params.put("clientId", clientId);
//签名
params.put("sign", SignUtil.sign(params, deviceSecret, "hmacMD5"));
//请求资源 mqtt
params.put("resources", "mqtt");
String result = AbstractAliyunWebUtils.doPost(authUrl, params, 5000, 5000);
LogUtil.print("result=[" + result + "]");
JSONObject mapResult;
try {
mapResult = JSON.parseObject(result);
} catch (Exception e) {
System.out.println("https auth result is invalid json fmt");
return;
}
if (RESULT_CODE_SUCCESS.equals(mapResult.getString(AUTH_RESULT_SUCCESS_KEY))) {
LogUtil.print("认证成功!" + mapResult.get("data"));
LogUtil.print("data=[" + mapResult + "]");
} else {
System.err.println("认证失败!");
throw new RuntimeException(
"认证失败:" + mapResult.get("code") + "," + mapResult.get("message"));
}
JSONObject data = (JSONObject)mapResult.get("data");
//sign TODO 服务器返回的sign签名 防止域名劫持验证
//mqtt服务器 TODO
String targetServer = "ssl://"
+ data.getJSONObject("resources").getJSONObject("mqtt")
.getString("host")
+ ":" + data.getJSONObject("resources").getJSONObject("mqtt")
.getString("port");
String token = data.getString("iotToken");
String iotId = data.getString("iotId");
//客户端ID格式:
/* 设备端自定义的标记,字符范围[0-9][a-z][A-Z] */
String mqttClientId = clientId;
/* 认证后得到的云端iotId */
String mqttUsername = iotId;
/* 认证后得到的token 有效期7天 */
String mqttPassword = token;
System.err.println("mqttclientId=" + mqttClientId);
connectMqtt(targetServer, mqttClientId, mqttUsername, mqttPassword);
}
private static void connectMqtt(String url, String clientId, String mqttUsername,
String mqttPassword) throws Exception {
MemoryPersistence persistence = new MemoryPersistence();
SSLSocketFactory socketFactory = createSSLSocket();
final MqttClient sampleClient = new MqttClient(url, clientId, persistence);
MqttConnectOptions connOpts = new MqttConnectOptions();
/* MQTT 3.1.1 */
connOpts.setMqttVersion(4);
connOpts.setSocketFactory(socketFactory);
//设置是否自动重连
connOpts.setAutomaticReconnect(true);
//如果是true 那么清理所有离线消息,即qos1 或者 2的所有未接收内容
connOpts.setCleanSession(false);
connOpts.setUserName(mqttUsername);
connOpts.setPassword(mqttPassword.toCharArray());
connOpts.setKeepAliveInterval(65);
LogUtil.print(clientId + "进行连接, 目的地: " + url);
//sampleClient.setManualAcks(true);//不要自动回执ack
sampleClient.connect(connOpts);
sampleClient.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
LogUtil.print("连接失败,原因:" + cause);
cause.printStackTrace();
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
LogUtil.print("接收到消息,来自Topic [" + topic + "], 内容是:["
+ new String(message.getPayload(), "UTF-8") + "], ");
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
//如果是qos 0消息 token.resp是没有回复的
LogUtil.print("消息发送成功! " + ((token == null || token.getResponse() == null) ? "null"
: token.getResponse().getKey()));
}
});
LogUtil.print("连接成功:---");
//订阅shadow topic
sampleClient.subscribe(shadowAckTopic, new IMqttMessageListener() {
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
LogUtil.print("收到消息:" + message + ",topic=" + topic);
JSONObject shadowJsonObj = JSON.parseObject(message.toString());
String shadowMethod = shadowJsonObj.getString("method");
JSONObject payloadJsonObj = shadowJsonObj.getJSONObject("payload");
/* method是reply,解析成功还是失败*/
if (SHADOW_METHOD_REPLY.equals(shadowMethod)) {
String status = payloadJsonObj.getString("status");
String stateInfo = payloadJsonObj.getString("state");
if (RESULT_STATUS_SUCCESS.equals(status)) {
if (stateInfo == null) {
System.out.println("update shadow success");
} else {
//解析出desired信息
parseDesiredMsg(message, sampleClient);
}
} else {
JSONObject errorJsonObj = payloadJsonObj.getJSONObject("content");
String errorCode = errorJsonObj.getString("errorcode");
String errorMsg = errorJsonObj.getString("errormessage");
//如果是影子没有内容,上报本次灯泡状态
if (RESULT_CODE_NO_SHADOW.equals(errorCode)){
System.out.println("影子是空的,显示白色,上报当前灯泡状态!");
//更新设备影子
Map attMap = new HashMap(128);
attMap.put("color", "white");
String shadowUpdateMsg = genUpdateShadowMsg(attMap);
System.out.println("updateShadowMsg: " + shadowUpdateMsg);
MqttMessage shadowMessage = new MqttMessage(shadowUpdateMsg.getBytes("UTF-8"));
message.setQos(1);
sampleClient.publish(shadowUpdateTopic, shadowMessage);
}else {
System.out.println("errorCode:" + errorCode);
System.out.println("errorMsg:" + errorMsg);
}
}
}
/* method是control,解析出desired和version信息 */
else if (SHADOW_METHOD_CONTROL.equals(shadowMethod)) {
parseDesiredMsg(message, sampleClient);
}
}
});
//获取影子内容,解析出version信息
String getShadowInfo = "{\"method\": \"get\"}";
MqttMessage shadowMessage = new MqttMessage(getShadowInfo.getBytes("UTF-8"));
shadowMessage.setQos(1);
sampleClient.publish(shadowUpdateTopic, shadowMessage);
//等待获取到版本号
Thread.sleep(1000);
}
private static SSLSocketFactory createSSLSocket() throws Exception {
SSLContext context = SSLContext.getInstance("TLSV1.2");
context.init(null, new TrustManager[] {new AliyunIotX509TrustManager()}, null);
SSLSocketFactory socketFactory = context.getSocketFactory();
return socketFactory;
}