物联网平台支持设置期望属性值,用以缓存设备状态。

背景信息

灯泡没有存储能力,如果从云端控制灯泡状态,需要设备一直在线。然而,实际情况下,灯泡很少一直在线。

物联网平台支持期望属性值的设置,可以解决这一问题。

灯泡每次打开时,读取存储于云端的期望属性值,根据期望值更新设备的实际属性,然后将更新后的属性上报至云端,存储为属性最新值。

创建产品和设备

  1. 登录物联网平台控制台
  2. 创建产品。本示例中,创建了路灯产品,所属类型选择为智能城市 / 公共服务 / 路灯照明


    物联网平台已为该类型产品预定义了物模型,可在产品的功能定义页签下查看。

  3. 创建设备,并获取设备证书(ProductKey、DeviceName、DeviceSecret)和地域(RegionId)信息。本示例中,在路灯产品下创建设备Lamp。

    设备创建成功后,可在设备的设备详情页,查看运行状态

    新建设备的最新属性值和期望属性值都为null,期望属性值版本为0。

在云端设置、查询期望属性值

在云端,您可以通过调用API,获取设备最新期望属性值和设置期望属性值控制设备。

具体操作可参考云端API文档。这里以Java SDK(云端)为例。

  • 调用QueryDeviceDesiredProperty,查看设备的期望属性值。
    DefaultProfile profile = DefaultProfile.getProfile(
            "<your-region-id>",          // 地域ID
            "<your-access-key-id>",      /阿里云账号的AccessKey ID
            "<your-access-key-secret>"); 阿里云账号Access Key Secret
    IAcsClient client = new DefaultAcsClient(profile);
    
    // 创建API请求并设置参数
    QueryDeviceDesiredPropertyRequest request = new QueryDeviceDesiredPropertyRequest();
    request.setProductKey(productKey);
    request.setDeviceName(deviceName);
    // 待查询的属性identifier列表。如不指定则查询所有属性(只读属性除外)的期望属性值。
    request.setIdentifiers(Arrays.asList("LightStatus"));
    
    // 发起请求并处理应答或异常
    QueryDeviceDesiredPropertyResponse response;
    try {
        response = client.getAcsResponse(request);
    
        QueryDeviceDesiredPropertyResponse.Data data = response.getData();
        for (QueryDeviceDesiredPropertyResponse.Data.DesiredPropertyInfo propInfo :
                data.getList()) {
            // 返回期望属性值及其版本,如 property=LightStatus desired=1 version=5
            System.out.println("property=" + propInfo.getIdentifier());
            System.out.println("desired=" + propInfo.getValue());
            System.out.println("version=" + propInfo.getBizVersion());
        }
    } catch (ServerException e) {
        e.printStackTrace();
    } catch (ClientException e) {
        e.printStackTrace();
    }
  • 调用SetDeviceDesiredProperty,设置期望属性值。
    DefaultProfile profile = DefaultProfile.getProfile(
            "<your-region-id>",          // 地域ID
            "<your-access-key-id>",     //阿里云账号的AccessKey ID
            "<your-access-key-secret>"); 阿里云账号AccessKey Secret
    IAcsClient client = new DefaultAcsClient(profile);
    
    // 创建API请求并设置参数
    SetDeviceDesiredPropertyRequest request = new SetDeviceDesiredPropertyRequest();
    request.setProductKey(productKey);
    request.setDeviceName(deviceName);
    // 待设置的属性identifier与期望属性值
    request.setItems("{\"LightStatus\": 1}");
    // 在处理多方并发控制设备的场景下可以使用期望属性值的版本,一般可以不指定版本
    request.setVersions("{}");
    
    // 发起请求并处理应答或异常
    SetDeviceDesiredPropertyResponse response;
    try {
        response = client.getAcsResponse(request);
    
        SetDeviceDesiredPropertyResponse.Data data = response.getData();
        // 如果设置成功,返回期望属性值的版本,如 {"LightStatus":5}
        System.out.println(data.getVersions());
    } catch (ServerException e) {
        e.printStackTrace();
    } catch (ClientException e) {
        e.printStackTrace();
    }

设备端开发

设备获取期望属性值,有两种场景:

  • 灯泡重新开启时,主动获取云端缓存的期望属性值。
  • 灯泡运行过程中,云端期望属性值发生改变,实时接收云端推送的期望属性值。

设备端开发更多信息请参考下载设备端SDK

本文使用Java SDK为例。

本文提供了完整的设备端Demo代码,请在本文最后一章中获取Demo。

  1. 填入设备证书和地域信息。
    /**
    * 设备证书信息
    */
    private static String productKey = "******";
    private static String deviceName = "********";
    private static String deviceSecret = "**************";
    /**
    * mqtt连接信息
    */
    private static String regionId = "******";
  2. 加入以下方法,用于变更实际灯泡的属性,并在属性变更后,主动将信息上报到最新属性值中。
    /**
     * 真实设备处理属性变更时,在以下两个场下会被调用:
     * 场景1. 设备联网后主动获取最新的属性期望值(由设备发起,拉模式)
     * 场景2. 设备在线时接收到云端property.set推送的属性期望值(由云端发起,推模式)
     * @param identifier  属性标识符
     * @param value       期望属性值
     * @param needReport  是否通过property.post发送状态上报。
     *                    上面场景2的处理函数中已集成属性上报能力,会将needReport设置为false
     * @return
     */
    private boolean handlePropertySet(String identifier, ValueWrapper value, boolean needReport) {
        ALog.d(TAG, "真实设备处理属性变更 = [" + identifier + "], value = [" + value + "]");
        // 用户根据实际情况判性是否设置成功 这里测试直接返回成功
        boolean success = true;
        if (needReport) {
            reportProperty(identifier, value);
        }
        return success;
    }
    
    private void reportProperty(String identifier, ValueWrapper value){
        if (StringUtils.isEmptyString(identifier) || value == null) {
            return;
        }
    
        ALog.d(TAG, "上报 属性identity=" + identifier);
    
        Map<String, ValueWrapper> reportData = new HashMap<>();
        reportData.put(identifier, value);
        LinkKit.getInstance().getDeviceThing().thingPropertyPost(reportData, new IPublishResourceListener() {
    
            public void onSuccess(String s, Object o) {
                // 属性上报成功
                ALog.d(TAG, "上报成功 onSuccess() called with: s = [" + s + "], o = [" + o + "]");
            }
    
            public void onError(String s, AError aError) {
                // 属性上报失败
                ALog.d(TAG, "上报失败onError() called with: s = [" + s + "], aError = [" + JSON.toJSONString(aError) + "]");
            }
        });
    }
  3. 灯泡在线时,如果云端设置了灯的期望属性值,该值将被推送到设备端。灯处理消息,改变状态。

    灯启动后,参考connectNotifyListener方法注册处理消息。相关Alink协议请参考设备属性、事件、服务章节。

    收到异步下行的数据后,mCommonHandler被调用,进而调用handlePropertySet更新设备的物理属性。

    /**
     * 注册服务调用(以及属性设置)的响应函数。
     * 云端调用设备的某项服务的时候,设备端需要响应该服务并回复。
     */
    public void connectNotifyListener() {
        List<Service> serviceList = LinkKit.getInstance().getDeviceThing().getServices();
        for (int i = 0; serviceList != null && i < serviceList.size(); i++) {
            Service service = serviceList.get(i);
            LinkKit.getInstance().getDeviceThing().setServiceHandler(service.getIdentifier(), mCommonHandler);
        }
    }
    
    private ITResRequestHandler mCommonHandler = new ITResRequestHandler() {
        public void onProcess(String serviceIdentifier, Object result, ITResResponseCallback itResResponseCallback) {
            ALog.d(TAG, "onProcess() called with: s = [" + serviceIdentifier + "]," +
                    " o = [" + result + "], itResResponseCallback = [" + itResResponseCallback + "]");
            ALog.d(TAG, "收到云端异步服务调用 " + serviceIdentifier);
            try {
                if (SERVICE_SET.equals(serviceIdentifier)) {
                    Map<String, ValueWrapper> data = (Map<String, ValueWrapper>)((InputParams)result).getData();
                    ALog.d(TAG, "收到异步下行数据 " + data);
                    // 设置真实设备的属性,然后上报设置完成的属性值
                    boolean isSetPropertySuccess =
                            handlePropertySet("LightStatus", data.get("LightStatus"), false);
                    if (isSetPropertySuccess) {
                        if (result instanceof InputParams) {
                            // 响应云端 接收数据成功
                            itResResponseCallback.onComplete(serviceIdentifier, null, null);
                        } else {
                            itResResponseCallback.onComplete(serviceIdentifier, null, null);
                        }
                    } else {
                        AError error = new AError();
                        error.setCode(100);
                        error.setMsg("setPropertyFailed.");
                        itResResponseCallback.onComplete(serviceIdentifier, new ErrorInfo(error), null);
                    }
                } else if (SERVICE_GET.equals(serviceIdentifier)) {
                } else {
                    // 根据不同的服务做不同的处理,跟具体的服务有关系
                    ALog.d(TAG, "根据真实的服务返回服务的值,请参照set示例");
                    OutputParams outputParams = new OutputParams();
                    // outputParams.put("op", new ValueWrapper.IntValueWrapper(20));
                    itResResponseCallback.onComplete(serviceIdentifier, null, outputParams);
                }
            } catch (Exception e) {
                e.printStackTrace();
                ALog.d(TAG, "云端返回数据格式异常");
            }
        }
    
        public void onSuccess(Object o, OutputParams outputParams) {
            ALog.d(TAG, "onSuccess() called with: o = [" + o + "], outputParams = [" + outputParams + "]");
            ALog.d(TAG, "注册服务成功");
        }
    
        public void onFail(Object o, ErrorInfo errorInfo) {
            ALog.d(TAG, "onFail() called with: o = [" + o + "], errorInfo = [" + errorInfo + "]");
            ALog.d(TAG, "注册服务失败");
        }
    };
  4. 灯泡离线时,如果云端设置了灯的期望属性值,该值将被存储在云端。

    灯泡连网后,主动获取期望属性值。获取后,将调用handlePropertySet更新实际设备的属性。

    LinkKit.getInstance().init(params, new ILinkKitConnectListener() {
        public void onError(AError aError) {
            ALog.e(TAG, "Init Error error=" + aError);
        }
        public void onInitDone(InitResult initResult) {
            ALog.i(TAG, "onInitDone result=" + initResult);
    
            connectNotifyListener();
    
            // 获取云端最新期望属性值
            getDesiredProperty(deviceInfo, Arrays.asList("LightStatus"), new IConnectSendListener() {
                public void onResponse(ARequest aRequest, AResponse aResponse) {
                    if(aRequest instanceof MqttPublishRequest && aResponse.data != null) {
                        JSONObject jsonObject = JSONObject.parseObject(aResponse.data.toString());
                        ALog.i(TAG, "onResponse result=" + jsonObject);
                        JSONObject dataObj = jsonObject.getJSONObject("data");
                        if (dataObj != null) {
                            if (dataObj.getJSONObject("LightStatus") == null) {
                                // 未设置期望值
                            } else {
                                Integer value = dataObj.getJSONObject("LightStatus").getInteger("value");
                                handlePropertySet("LightStatus", new ValueWrapper.IntValueWrapper(value), true);
                            }
                        }
                    }
                }
                public void onFailure(ARequest aRequest, AError aError) {
                    ALog.d(TAG, "onFailure() called with: aRequest = [" + aRequest + "], aError = [" + aError + "]");
                }
            });
        }
    });
    
    private void getDesiredProperty(BaseInfo info, List<String> properties, IConnectSendListener listener) {
        ALog.d(TAG, "getDesiredProperty() called with: info = [" + info + "], listener = [" + listener + "]");
        if(info != null && !StringUtils.isEmptyString(info.productKey) && !StringUtils.isEmptyString(info.deviceName)) {
            MqttPublishRequest request = new MqttPublishRequest();
            request.topic = DESIRED_PROPERTY_GET.replace("{productKey}", info.productKey).replace("{deviceName}", info.deviceName);
            request.replyTopic = DESIRED_PROPERTY_GET_REPLY.replace("{productKey}", info.productKey).replace("{deviceName}", info.deviceName);
            request.isRPC = true;
            RequestModel<List<String>> model = new RequestModel<>();
            model.id = String.valueOf(IDGeneraterUtils.getId());
            model.method = METHOD_GET_DESIRED_PROPERTY;
            model.params = properties;
            model.version = "1.0";
            request.payloadObj = model.toString();
            ALog.d(TAG, "getDesiredProperty: payloadObj=" + request.payloadObj);
            ConnectSDK.getInstance().send(request, listener);
        } else {
            ALog.w(TAG, "getDesiredProperty failed, baseInfo Empty.");
            if(listener != null) {
                AError error = new AError();
                error.setMsg("BaseInfoEmpty.");
                listener.onFailure(null, error);
            }
        }
    }

验证操作

待验证结果:无论灯泡在线或者离线,都可以通过设置期望属性值成功更改设备状态。

  • 设备在线时,服务端修改灯泡开关状态,灯泡实时响应状态变化。

  • 设备不在线时,如果服务端修改灯泡开关状态,云端期望属性值与设备的最新属性值不一致。

  • 设备联网后,设备主动拉取期望属性值,设备的最新属性值实现与云端期望属性值的同步。

总结:只需修改期望属性值即可改变灯泡的开关状态。没有存储能力的灯泡,通过期望属性值实现了灯泡开关状态的控制。

附录:设备端Demo代码

package com.aliyun.alink.devicesdk.demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.alink.apiclient.utils.StringUtils;
import com.aliyun.alink.dm.api.BaseInfo;
import com.aliyun.alink.dm.api.DeviceInfo;
import com.aliyun.alink.dm.api.InitResult;
import com.aliyun.alink.dm.model.RequestModel;
import com.aliyun.alink.dm.utils.IDGeneraterUtils;
import com.aliyun.alink.linkkit.api.ILinkKitConnectListener;
import com.aliyun.alink.linkkit.api.IoTMqttClientConfig;
import com.aliyun.alink.linkkit.api.LinkKit;
import com.aliyun.alink.linkkit.api.LinkKitInitParams;
import com.aliyun.alink.linksdk.cmp.api.ConnectSDK;
import com.aliyun.alink.linksdk.cmp.connect.channel.MqttPublishRequest;
import com.aliyun.alink.linksdk.cmp.core.base.ARequest;
import com.aliyun.alink.linksdk.cmp.core.base.AResponse;
import com.aliyun.alink.linksdk.cmp.core.listener.IConnectSendListener;
import com.aliyun.alink.linksdk.tmp.api.InputParams;
import com.aliyun.alink.linksdk.tmp.api.OutputParams;
import com.aliyun.alink.linksdk.tmp.device.payload.ValueWrapper;
import com.aliyun.alink.linksdk.tmp.devicemodel.Service;
import com.aliyun.alink.linksdk.tmp.listener.IPublishResourceListener;
import com.aliyun.alink.linksdk.tmp.listener.ITResRequestHandler;
import com.aliyun.alink.linksdk.tmp.listener.ITResResponseCallback;
import com.aliyun.alink.linksdk.tmp.utils.ErrorInfo;
import com.aliyun.alink.linksdk.tools.AError;
import com.aliyun.alink.linksdk.tools.ALog;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class LampDemo {
    private static final String TAG = "LampDemo";

    private final static String SERVICE_SET = "set";
    private final static String SERVICE_GET = "get";

    public static String DESIRED_PROPERTY_GET = "/sys/{productKey}/{deviceName}/thing/property/desired/get";
    public static String DESIRED_PROPERTY_GET_REPLY = "/sys/{productKey}/{deviceName}/thing/property/desired/get_reply";
    public static String METHOD_GET_DESIRED_PROPERTY = "thing.property.desired.get";

    public static void main(String[] args) {
        /**
         * 设备证书信息
         */
        String productKey = "****";
        String deviceName = "Lamp";
        String deviceSecret = "****";
        /**
         * mqtt连接信息
         */
        String regionId = "cn-shanghai";

        LampDemo manager = new LampDemo();

        DeviceInfo deviceInfo = new DeviceInfo();
        deviceInfo.productKey = productKey;
        deviceInfo.deviceName = deviceName;
        deviceInfo.deviceSecret = deviceSecret;

        manager.init(deviceInfo, regionId);
    }

    public void init(final DeviceInfo deviceInfo, String region) {
        LinkKitInitParams params = new LinkKitInitParams();
        /**
         * 设置 Mqtt 初始化参数
         */
        IoTMqttClientConfig config = new IoTMqttClientConfig();
        config.productKey = deviceInfo.productKey;
        config.deviceName = deviceInfo.deviceName;
        config.deviceSecret = deviceInfo.deviceSecret;
        config.channelHost = deviceInfo.productKey + ".iot-as-mqtt." + region + ".aliyuncs.com:1883";
        /**
         * 是否接受离线消息
         * 对应 mqtt 的 cleanSession 字段
         */
        config.receiveOfflineMsg = false;
        params.mqttClientConfig = config;

        /**
         * 设置初始化,传入设备证书信息
         */
        params.deviceInfo = deviceInfo;

        LinkKit.getInstance().init(params, new ILinkKitConnectListener() {
            public void onError(AError aError) {
                ALog.e(TAG, "Init Error error=" + aError);
            }
            public void onInitDone(InitResult initResult) {
                ALog.i(TAG, "onInitDone result=" + initResult);

                connectNotifyListener();

                // 获取云端最新期望属性值
                getDesiredProperty(deviceInfo, Arrays.asList("LightStatus"), new IConnectSendListener() {
                    public void onResponse(ARequest aRequest, AResponse aResponse) {
                        if(aRequest instanceof MqttPublishRequest && aResponse.data != null) {
                            JSONObject jsonObject = JSONObject.parseObject(aResponse.data.toString());
                            ALog.i(TAG, "onResponse result=" + jsonObject);
                            JSONObject dataObj = jsonObject.getJSONObject("data");
                            if (dataObj != null) {
                                if (dataObj.getJSONObject("LightStatus") == null) {
                                    // 未设置期望值
                                } else {
                                    Integer value = dataObj.getJSONObject("LightStatus").getInteger("value");
                                    handlePropertySet("LightStatus", new ValueWrapper.IntValueWrapper(value), true);
                                }
                            }
                        }
                    }
                    public void onFailure(ARequest aRequest, AError aError) {
                        ALog.d(TAG, "onFailure() called with: aRequest = [" + aRequest + "], aError = [" + aError + "]");
                    }
                });
            }
        });
    }

    /**
     * 真实设备处理属性变更,两个场景下会被调用:
     * 场景1. 设备联网后主动获取最新的属性期望值(由设备发起,拉模式)
     * 场景2. 设备在线时接收到云端property.set推送(由云端发起,推模式)
     * @param identifier  属性标识符
     * @param value       期望属性值
     * @param needReport  是否发送property.post状态上报。
     *                    上面场景2的处理函数中已集成属性上报能力,会将needReport设置为false
     * @return
     */
    private boolean handlePropertySet(String identifier, ValueWrapper value, boolean needReport) {
        ALog.d(TAG, "真实设备处理属性变更 = [" + identifier + "], value = [" + value + "]");
        // 用户根据实际情况判断属性是否设置成功 这里测试直接返回成功
        boolean success = true;
        if (needReport) {
            reportProperty(identifier, value);
        }
        return success;
    }

    private void reportProperty(String identifier, ValueWrapper value){
        if (StringUtils.isEmptyString(identifier) || value == null) {
            return;
        }

        ALog.d(TAG, "上报 属性identity=" + identifier);

        Map<String, ValueWrapper> reportData = new HashMap<>();
        reportData.put(identifier, value);
        LinkKit.getInstance().getDeviceThing().thingPropertyPost(reportData, new IPublishResourceListener() {

            public void onSuccess(String s, Object o) {
                // 属性上报成功
                ALog.d(TAG, "上报成功 onSuccess() called with: s = [" + s + "], o = [" + o + "]");
            }

            public void onError(String s, AError aError) {
                // 属性上报失败
                ALog.d(TAG, "上报失败onError() called with: s = [" + s + "], aError = [" + JSON.toJSONString(aError) + "]");
            }
        });
    }

    /**
     * 注册服务调用(以及属性设置)的响应函数。
     * 云端调用设备的某项服务的时候,设备端需要响应该服务并回复。
     */
    public void connectNotifyListener() {
        List<Service> serviceList = LinkKit.getInstance().getDeviceThing().getServices();
        for (int i = 0; serviceList != null && i < serviceList.size(); i++) {
            Service service = serviceList.get(i);
            LinkKit.getInstance().getDeviceThing().setServiceHandler(service.getIdentifier(), mCommonHandler);
        }
    }

    private ITResRequestHandler mCommonHandler = new ITResRequestHandler() {
        public void onProcess(String serviceIdentifier, Object result, ITResResponseCallback itResResponseCallback) {
            ALog.d(TAG, "onProcess() called with: s = [" + serviceIdentifier + "]," +
                    " o = [" + result + "], itResResponseCallback = [" + itResResponseCallback + "]");
            ALog.d(TAG, "收到云端异步服务调用 " + serviceIdentifier);
            try {
                if (SERVICE_SET.equals(serviceIdentifier)) {
                    Map<String, ValueWrapper> data = (Map<String, ValueWrapper>)((InputParams)result).getData();
                    ALog.d(TAG, "收到异步下行数据 " + data);
                    // 设置真实设备的属性,然后上报设置完成的属性值
                    boolean isSetPropertySuccess =
                            handlePropertySet("LightStatus", data.get("LightStatus"), false);
                    if (isSetPropertySuccess) {
                        if (result instanceof InputParams) {
                            // 响应云端 接收数据成功
                            itResResponseCallback.onComplete(serviceIdentifier, null, null);
                        } else {
                            itResResponseCallback.onComplete(serviceIdentifier, null, null);
                        }
                    } else {
                        AError error = new AError();
                        error.setCode(100);
                        error.setMsg("setPropertyFailed.");
                        itResResponseCallback.onComplete(serviceIdentifier, new ErrorInfo(error), null);
                    }
                } else if (SERVICE_GET.equals(serviceIdentifier)) {
                } else {
                    // 根据不同的服务做不同的处理,跟具体的服务有关系
                    ALog.d(TAG, "用户根据真实的服务返回服务的值,请参照set示例");
                    OutputParams outputParams = new OutputParams();
                    // outputParams.put("op", new ValueWrapper.IntValueWrapper(20));
                    itResResponseCallback.onComplete(serviceIdentifier, null, outputParams);
                }
            } catch (Exception e) {
                e.printStackTrace();
                ALog.d(TAG, "云端返回数据格式异常");
            }
        }

        public void onSuccess(Object o, OutputParams outputParams) {
            ALog.d(TAG, "onSuccess() called with: o = [" + o + "], outputParams = [" + outputParams + "]");
            ALog.d(TAG, "注册服务成功");
        }

        public void onFail(Object o, ErrorInfo errorInfo) {
            ALog.d(TAG, "onFail() called with: o = [" + o + "], errorInfo = [" + errorInfo + "]");
            ALog.d(TAG, "注册服务失败");
        }
    };

    private void getDesiredProperty(BaseInfo info, List<String> properties, IConnectSendListener listener) {
        ALog.d(TAG, "getDesiredProperty() called with: info = [" + info + "], listener = [" + listener + "]");
        if(info != null && !StringUtils.isEmptyString(info.productKey) && !StringUtils.isEmptyString(info.deviceName)) {
            MqttPublishRequest request = new MqttPublishRequest();
            request.topic = DESIRED_PROPERTY_GET.replace("{productKey}", info.productKey).replace("{deviceName}", info.deviceName);
            request.replyTopic = DESIRED_PROPERTY_GET_REPLY.replace("{productKey}", info.productKey).replace("{deviceName}", info.deviceName);
            request.isRPC = true;
            RequestModel<List<String>> model = new RequestModel<>();
            model.id = String.valueOf(IDGeneraterUtils.getId());
            model.method = METHOD_GET_DESIRED_PROPERTY;
            model.params = properties;
            model.version = "1.0";
            request.payloadObj = model.toString();
            ALog.d(TAG, "getDesiredProperty: payloadObj=" + request.payloadObj);
            ConnectSDK.getInstance().send(request, listener);
        } else {
            ALog.w(TAG, "getDesiredProperty failed, baseInfo Empty.");
            if(listener != null) {
                AError error = new AError();
                error.setMsg("BaseInfoEmpty.");
                listener.onFailure(null, error);
            }
        }
    }
}