开发App端的本地定时功能

本地定时相对云端定时而言,是指当设备离线时,也能自动执行定时任务。本文档介绍自有App中的定时功能的开发实践。

前提条件

已完成开发设备端本地定时功能中的配置控制台参数和设备端开发。

概述

生活物联网平台提供App 基于Native开发定时功能(静态方式) 的开发方式。

Native开发定时功能(静态方式)

静态方式开发即开发native的本地定时功能。该方式开发的App定时功能用户体验更好,但对技术要求较高,且无法动态更新,需要通过发布App版本来更新。

Native开发定时功能(iOS)

  1. 创建一个自有App,并完成SDK下载。详细操作请参见创建自有App

  2. 获取本地定时的属性字段。

    本地定时是属于TSL的一个属性,定时就是操作TSL的LocalTimer。根据物模型SDK获取对应设备的TSL,并从中获取本地定时的个数、Targets、Timezoneoffset属性字段。

    // 获取本地定时的个数,是否支持 Targets、Timezoneoffset
    NSUInteger size = 0;
    IMSThing *thing = [[IMSThingManager sharedManager] buildThing:iotId];
    IMSThingProfile *profile = [thing getThingProfile];
    NSArray<IMSThingTslProperty *> *proList = [profile allPropertiesOfModel];
    __block BOOL hasTargets = NO;
    __block BOOL hasTimezoneOffset = NO;
    
    for (NSUInteger i = 0; i<proList.count; i++) {
        IMSThingTslProperty * _Nonnull obj = proList[i];
        NSLog(@"%lul", (unsigned long)i);
        if ([obj.identifier isEqualToString:@"LocalTimer"]) {
            NSString *sizeTmp = obj.dataType[@"specs"][@"size"];
            size = sizeTmp.integerValue;
    
            NSArray *itemList = obj.dataType[@"specs"][@"item"][@"specs"];
            [itemList enumerateObjectsUsingBlock:^(id  _Nonnull item, NSUInteger idx, BOOL * _Nonnull stop) {
                if ([item[@"identifier"] isEqualToString:@"Targets"]) {
                    hasTargets = YES;
                } else if ([item[@"identifier"] isEqualToString:@"TimezoneOffset"]){
                    hasTimezoneOffset = YES;
                }
            }];
        }
    }
  3. 获取设备端的定时器列表数据。

    // 本地定时model数据结构
    @interface IMSLocalTimerModel : NSObject
    @property (nonatomic, copy)   NSString *timer;
    @property (nonatomic, assign) BOOL enable;
    @property (nonatomic, assign) BOOL isValid;
    @property (nonatomic, strong) NSMutableDictionary *propertyValueDic;
    @property (nonatomic, strong) NSString *iotId;
    @property (nonatomic, strong, nullable) NSString *targets;
    @property (nonatomic, assign) NSInteger timezoneOffset;
    @end
    
    // 获取本地定时列表数据
    id<IMSThingActions> thingObj = [thing getThingActions];
    NSMutableArray<IMSLocalTimerModel *> *list = [NSMutableArray array];
    
    [thingObj getPropertiesFull:^(IMSThingActionsResponse * _Nullable response) {
        // 回调不在主线程,请注意
        NSDictionary *dic = (NSDictionary *)response.dataObject;
        NSDictionary *data = dic[@"data"];
        for (NSString *key in data){
            if ([key isEqualToString:@"LocalTimer"]){
                NSDictionary *localTimer = data[key];
                NSArray *value = localTimer[@"value"];
                for (NSDictionary *item in value) {
                    IMSLocalTimerModel *timer = [IMSLocalTimerModel new];
                    timer.isValid = NO;
                    timer.propertyValueDic = [NSMutableDictionary dictionary];
                    for (NSString *a in item) {
                        if ([a isEqualToString:@"IsValid"]) {
                            NSNumber *isValid = item[a];
                            timer.isValid = isValid.boolValue;
                        } else if ([a isEqualToString:@"Enable"]) {
                            NSNumber *enable = item[a];
                            timer.enable = enable.boolValue;
                        } else if ([a isEqualToString:@"Timer"]) {
                            timer.timer = item[a];
                        } else if ([a isEqualToString:@"Targets"]){
                            timer.targets = item[a];
                        } else if ([a isEqualToString:@"TimezoneOffset"]){
                            NSNumber *timezoneOffset = item[a];
                            timer.timezoneOffset = timezoneOffset.integerValue;
                        } else {
                            timer.propertyValueDic[a] = item[a];
                        }
                    }
    
                    if (!timer.isValid) {
                        timer.propertyValueDic = [NSMutableDictionary dictionary];
                        timer.timer = @"";
                        timer.targets = @"";
                        timer.timezoneOffset = 0;
                    }
    
                    timer.hasTargets = hasTargets;
                    timer.hasTimezoneOffset = hasTimezoneOffset;
                    [list addObject:timer];
                }
    
                break;
            }
        }
    
        // 如果get到的属性值的timer个数没有达到tsl要求的定时器数,需要补齐,因为set的时候,tsl要求返回的个数和tsl要求的一样
        for (NSInteger i = list.count; i < size; i++) {
            IMSLocalTimerModel *timer = [IMSLocalTimerModel new];
            timer.isValid = NO;
            timer.timer = @"";
            timer.iotId = iotId;
            timer.hasTargets = hasTargets;
            timer.hasTimezoneOffset = hasTimezoneOffset;
            timer.propertyValueDic = [NSMutableDictionary dictionary];
            timer.modelList = list;
            [list addObject:timer];
        }
    }
  4. 设置(编辑/创建/删除等)一个定时器。

    @implementation IMSLocalTimerModel
    // 单个定时转换为json接口,供参考
    - (NSMutableDictionary *)toJson{
        NSMutableDictionary *json = [NSMutableDictionary dictionary];
    
        [json addEntriesFromDictionary:self.propertyValueDic];
        [json addEntriesFromDictionary:@{@"Enable":@(self.enable?1:0),@"IsValid":@(self.isValid?1:0), @"Timer":self.timer?:@""}];
    
        if (self.hasTargets) {
        [json addEntriesFromDictionary:@{@"Targets":self.targets?:@""}];
        }
    
        if (self.hasTimezoneOffset) {
        [json addEntriesFromDictionary:@{@"TimezoneOffset":@(self.timezoneOffset)}];
        }
    
        return json;
    }
    
    /* 如果 TimezoneOffset 字段存在,这个字段是为了让设备端能获取到app端设置定时时所处时区*/
    propertyValues[@"TimezoneOffset"] = @([NSTimeZone localTimeZone].secondsFromGMT);
    
    /*
        假设当前设备有3个属性可以通过本地定时进行控制,这3个属性的identifier分别为id1、id2、id3
        如果本地定时存在Targets字段,则允许1个定时只控制3个属性中部分属性,假设这个定时是控制属性id1和属性id2,
        则 Targets = @"id1, id2"
    
        如果本地定时不存在Targets字段,则要求改定时创建或者编辑的时候,必须对3个属性都设置属性值(定时时间到的是属性需要生效的值)
    */
    
    // list 是属性的 identifier 的列表
    - (NSString *)setTargetList:(NSArray<NSString *> *)list{
        if (list.count == 0) {
            return @"";
        }
    
        NSString *targets = list.firstObject;
        for (NSUInteger i = 1; i < list.count; i++) {
            targets = [targets stringByAppendingString:@","];
            targets = [targets stringByAppendingString:list[i]];
        }
    
        return targets;
    }
    
    propertyValues[@"TimezoneOffset"] = [obj setTargetList:@[id1, id2]];
    
    /* 假设tsl中获取到设备可以设置3个本地定时,则timerList=@[[timer1 toJson], [timer2 toJson], [timer3 toJson]] :
        每个定时器的json生成参考 [IMSLocalTimerModel toJson]
    [
    1. 同时控制属性1和属性2的定时
    {"id1":1,"id2":2,"id3":1,"Timer":"0 8 * * *","Enable":1,"IsValid":1, "Targets":"id1, id2", "TimezoneOffset":""},
    2. 只控制属性1的定时
     {"id1":1,"id2":2,"id3":1,"Timer":"2 6 * * 3","Enable":1,"IsValid":1, "Targets":"id1", "TimezoneOffset":""},
    3. 设置一个定时的IsValid为0,则就是删除该定时器,但是这里建议把IsValid 也设置为0,因为设备端的实现可能只认IsValid字段
     {"id1":1,"id2":2,"id3":1,"Timer":"2 6 * * 3","Enable":0,"IsValid":0, "Targets":"", "TimezoneOffset":""}
    ]
    */
    [thingActions setProperties:@{@"LocalTimer":timerList} responseHandler:^(IMSThingActionsResponse * _Nullable response) {
    }
  5. 订阅本地定时属性值。

    以监听定时的Enable状态为例,设定定时的有效性是一次,当定时超时后,App端会收到定时的Enable属性变为0的通知,并进行界面刷新等相关操作。相应示例代码如下。

    // 代码供参考
    a.注册订阅
    IMSThing *thing = [[IMSThingManager sharedManager] buildThing:controller.iotId];
    [thing registerThingObserver:(id<IMSThingObserver>)obj];
    
    b.订阅通知处理
    - (void)onPropertyChange:(NSString *)iotId params:(NSDictionary *)params{
        IMSDeviceLogDebug(@"onPropertyChange %@ %@", iotId, params);
        NSArray *list = params[@"items"][@"LocalTimer"][@"value"];
        for (NSInteger i = 0; i < list.count; i++) {
            NSDictionary *timer = list[i];
            if (i >= self.list.count) {
                [self.list addObject:[IMSLocalTimerModel new]];
            }
    
            IMSLocalTimerModel *model = self.list[i];
            model.timezoneOffset = NO;
            model.hasTargets = NO;
            model.propertyValueDic = [NSMutableDictionary dictionary];
            [timer enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, NSNumber*  _Nonnull obj, BOOL * _Nonnull stop) {
                if ([key isEqualToString:@"Enable"]) {
                    model.enable = obj.boolValue;
                } else if ([key isEqualToString:@"IsValid"]){
                    model.isValid = obj.boolValue;
                } else if ([key isEqualToString:@"Targets"]){
                    model.targets = (NSString *)obj;
                    model.hasTargets = YES;
                } else if ([key isEqualToString:@"Timer"]){
                    model.timer = (NSString *)obj;
                } else if ([key isEqualToString:@"TimezoneOffset"]){
                    model.timezoneOffset = obj.integerValue;
                    model.hasTimezoneOffset = YES;
                } else {
                    model.propertyValueDic[key] = obj;
                }
            }];
        }
    
        // 刷新界面等
    }
    
    c.注销订阅
    IMSThing *thing = [[IMSThingManager sharedManager] buildThing:self.iotId];
    [thing unregisterThingObserver:self];

Native开发定时功能(Android)

  1. 创建一个自有App,并完成SDK下载。详细操作请参见创建自有App

  2. 获取设备TSL模型和定时属性。

    1. 创建com.aliyun.alink.linksdk.tmp.device.panel.PanelDevice对象。

      PanelDevice panelDevice = new PanelDevice(iotId);
      panelDevice.init(null, new IPanelCallback() {
        @Override
        public void onComplete(boolean succeed, Object json) {
            // 根据自己的业务逻辑实现相关代码
        }
      })
    2. 获取TSL模型。

      本地定时是属于TSL的一个属性,更多TSL模型的介绍请参见物模型SDK

      panelDevice.getTslByCache(new IPanelCallback() {
          @Override
          public void onComplete(boolean succeed, Object json) {
        // 根据自己的业务逻辑实现相关代码
          }
      });
    3. 获取设备属性。

      panelDevice.getPropertiesByCache(new IPanelCallback() {
        @Override
        public void onComplete(boolean succeed, @Nullable Object json) {
           // 根据自己的业务逻辑实现相关代码
          }
      }, null);

      返回数据中的JSON格式示例如下。

      {
          "items":{
          "LocalTimer":  // 以下为定时数据的示例
      [{
                  "LightSwitch": 1,
                  "ColorTemperature": 2000,
                  "Timer": "5 4 1,2,3",
                  "TimezoneOffset": 43200,
                  "Brightness": 0,
                  "Enable": 1,
                  "Targets": "LightSwitch",
                  "WorkMode": 0,
                  "IsValid": 1
      }]
                  }
      }
  3. 解析定时数据。

    根据TSL模型解析定时数据的方法如下,更详细的数据结构请参见物模型SDK

       static private void parseLocalTimer(JSONObject dataObj, LocalTimerData localTimerData) {
            for (String key : dataObj.keySet()) {
                if (key.equals("LocalTimer")) {
                    JSONArray value = dataObj.getJSONObject(key).getJSONArray("value");
                    for (int i = 0; i < value.size(); i++) {
                        JSONObject valueItem = value.getJSONObject(i);
                        LocalTimer localTimer = new LocalTimer();
                        for (String valueItemkey : valueItem.keySet()) {
                            if (valueItemkey.equals("Timer")) {
                                localTimer.timer = (String) valueItem.get(valueItemkey);
                            } else if (valueItemkey.equals("Enable")) {
                                localTimer.enable = (int) valueItem.get(valueItemkey);
                            } else if (valueItemkey.equals("IsValid")) {
                                localTimer.valid = (int) valueItem.get(valueItemkey);
                            } else if (valueItemkey.equals("Targets")) {
                                localTimer.targets = (String) valueItem.get(valueItemkey);
                            } else if (valueItemkey.equals("TimezoneOffset")) {
                                localTimer.timezoneOffset = (int) valueItem.get(valueItemkey);
                            } else {
                                localTimer.property.put(valueItemkey, valueItem.get(valueItemkey));
                            }
                        }
                        localTimerData.items.add(localTimer);                }
                }
         } 
  4. 设置一个定时器。

    • 单设备

      使用PanelDevice#setProperties()方法更新设备属性。

      panelDevice.setProperties(json, new IPanelCallback() {
                      @Override
                      public void onComplete(final boolean succeed, final Object json) {
                    }
                  });

      入参JSON格式示例如下。

      {
          "iotId":"",
          "items":{
              "LocalTimer":[  // 以下为定时数据的示例
                  {
                      "LightSwitch":1,
                      "ColorTemperature":2000,
                      "Timer":"5 4 1,2,3",
                      "TimezoneOffset":43200,
                      "Brightness":0,
                      "Enable":1,
                      "Targets":"LightSwitch",
                      "WorkMode":0,
                      "IsValid":1
                  }
              ]
          }
      }
    • 组控设备

      使用PanelGroup#setGroupProperties()方法更新设备属性。

      PanelGroup的初始化方法与panelDevice的初始化类似,区别是要传入组控的groupId

       panelGroup.setGroupProperties(json, new IPanelCallback() {
                      @Override
                      public void onComplete(final boolean succeed, final Object json) {
                    }
       });

      入参JSON格式示例如下。

      {
          "controlGroupId":"",
          "items":{
              "LocalTimer":[   // 以下为定时数据的示例
                  {
                      "LightSwitch":1,
                      "ColorTemperature":2000,
                      "Timer":"5 4 1,2,3",
                      "TimezoneOffset":43200,
                      "Brightness":0,
                      "Enable":1,
                      "Targets":"LightSwitch",
                      "WorkMode":0,
                      "IsValid":1
                  }
              ]
          }
      }
  5. 订阅本地定时属性值。

    实时更新常用来处理多端同时设置设备属性的情况。

    // 接口:/app/down/thing/properties 设备端上报属性
    panelDevice.subAllEvents(new IPanelEventCallback() {
                        @Override
                        public void onNotify(String id, String path, Object json) {
                            if (!id.equals(iotId)) {
                                return;
                            }
                            if (!"/app/down/thing/properties".equals(path)) {
                                return;
                            }
    
                        }
                    }, new IPanelCallback() {
                        @Override
                        public void onComplete(boolean b, Object o) {
                        }
                    });

    返回数据中的JSON格式示例如下。

    {
        "items":{
        "LocalTimer":  // 以下为定时数据的示例
    [{
                "LightSwitch": 1,
                "ColorTemperature": 2000,
                "Timer": "5 4 1,2,3",
                "TimezoneOffset": 43200,
                "Brightness": 0,
                "Enable": 1,
                "Targets": "LightSwitch",
                "WorkMode": 0,
                "IsValid": 1
    }]
                }
    }

通用说明

  • corn 表达式说明(即LocalTimer结构中的Timer字段 )

    定时属性中的 CronTrigger 配置完整格式为: [分] [小时] [日] [月] [周]

    • *表示所有值。在分钟里表示每一分钟触发。如在小时、日期、月份里面表示每一小时、每一日、每一月。

    • - 表示区间。小时设置为10-12表示10、11、12点均会触发。

    • ,表示多个值。 周设置成 2、3、4、5、6 表示在周一至周五工作日会触发。

    • /表示递增触发。 5/15表示从第5秒开始,每隔15秒触发。

    • L表示最后的意思。 日上表示最后一天。星期上表示星期六或7。 L前加数据,表示该数据的最后一个。 星期上设置6L表示最后一个星期五(6表示星期五)。

    • W表示离指定日期最近的工作日触发。15W离该月15号最近的工作日触发。表示每月的第几个周几(6#3表示该月的第三个周五)。

  • 时区说明(即LocalTimer结构中的TimeOffset字段 )

    由于Android中自带的Calendar对于Daylight Saving Time (DST)的处理有问题。在需要处理冬令时和夏令时的地区,请使用Java 8提供的Instant类或者其他方法来计算时差。代码示例如下。

     private int timezoneOffset() {
            try {
                Instant instant = Instant.now();
                Calendar calendar = new GregorianCalendar();
                TimeZone timezone = calendar.getTimeZone();
                ZoneId zone = ZoneId.of(timezone.getID());
                ZonedDateTime z = instant.atZone(zone);
                int offset = z.getOffset().getTotalSeconds();
                ALog.d(TAG, "timezoneOffset(): ZoneId:" + timezone.getID() + ", getTotalSeconds: " + offset);
                return offset;
            } catch (Exception ignored) {
                return 0;
            }
        }
  • Targets字段说明

    如果在LocalTimer里添加了多个动作, 则必须在Target字段里面添加您本次修改的字段。否则,您必须完整设置所有的动作,本地定时才能正常保存。

    {
        "LightSwitch":0,
        "Timer":"45 12 * * *",
        "Enable":0,
        "Targets":"LightSwitch",
        "IsValid":1
    }