Developing the local scheduled task feature for an app

更新时间:
复制 MD 格式

Local scheduled tasks run automatically even when a device is offline. This is different from cloud-based scheduled tasks. This document describes how to develop the local scheduled task feature for your proprietary app.

Prerequisites

Complete the console parameter configuration and device-side development described in Develop the local scheduled task feature for a device.

Overview

IoT Platform supports native development of the scheduled task feature for your app using a static method.

Native development for the scheduled task feature (static method)

The static method involves developing a native local scheduled task feature. This method provides an improved user experience but has higher technical requirements. It does not support dynamic updates, which means you must release a new app version to apply any changes.

Native development for the scheduled task feature (iOS)

  1. Create a proprietary app and download the software development kit (SDK). For more information, see Create a proprietary app.

  2. Retrieve the property fields for local scheduled tasks.

    Local scheduled tasks are a property of the Thing Specification Language (TSL) model. Scheduling a task involves operating the LocalTimer property of the TSL model. Use the TSL model SDK to retrieve the TSL model for the device. Then, retrieve the number of local timers and the Targets and Timezoneoffset property fields.

    // Get the number of local timers and check if Targets and Timezoneoffset are supported.
    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. Retrieve the list of timers from the device.

    // Data structure of the local timer 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
    
    // Get the local timer list data.
    id<IMSThingActions> thingObj = [thing getThingActions];
    NSMutableArray<IMSLocalTimerModel *> *list = [NSMutableArray array];
    
    [thingObj getPropertiesFull:^(IMSThingActionsResponse * _Nullable response) {
        // Note: The callback is not in the main thread.
        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;
            }
        }
    
        // If the number of timers obtained from the properties is less than the number required by the TSL model, you must add more timers. This is because the set operation requires the number of returned timers to match the number specified in the TSL model.
        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. Set a timer (edit, create, or delete).

    @implementation IMSLocalTimerModel
    // Convert a single timer to a JSON object. This is for reference only.
    - (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;
    }
    
    /* If the TimezoneOffset field exists, it allows the device to get the time zone of the app when the timer was set. */
    propertyValues[@"TimezoneOffset"] = @([NSTimeZone localTimeZone].secondsFromGMT);
    
    /*
        Assume the device has three properties that can be controlled by local timers. The identifiers of these properties are id1, id2, and id3.
        If the local timer has a Targets field, a single timer can control a subset of these properties. For example, if a timer controls properties id1 and id2, then Targets = @"id1, id2".
    
        If the local timer does not have a Targets field, you must set values for all three properties when you create or edit the timer. These are the values that the properties will take when the timer triggers.
    */
    
    // list is a list of property identifiers.
    - (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]];
    
    /* Assume the TSL model indicates that the device can have three local timers. Then, timerList=@[[timer1 toJson], [timer2 toJson], [timer3 toJson]].
        For information about how to generate the JSON object for each timer, see [IMSLocalTimerModel toJson].
    [
    1. A timer that controls both property 1 and property 2.
    {"id1":1,"id2":2,"id3":1,"Timer":"0 8 * * *","Enable":1,"IsValid":1, "Targets":"id1, id2", "TimezoneOffset":""},
    2. A timer that controls only property 1.
     {"id1":1,"id2":2,"id3":1,"Timer":"2 6 * * 3","Enable":1,"IsValid":1, "Targets":"id1", "TimezoneOffset":""},
    3. To delete a timer, set its IsValid value to 0. We recommend that you also set Enable to 0, because the device-side implementation might only recognize the IsValid field.
     {"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. Subscribe to local timer property values.

    This example shows how to listen for the Enable status of a timer. The timer is set to run only once. After the timer triggers, the app receives a notification that the Enable property has changed to 0. The app then refreshes the UI. The following is sample code.

    // The following code is for reference only.
    a. Register a subscription.
    IMSThing *thing = [[IMSThingManager sharedManager] buildThing:controller.iotId];
    [thing registerThingObserver:(id<IMSThingObserver>)obj];
    
    b. Process subscription notifications.
    - (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;
                }
            }];
        }
    
        // Refresh the UI.
    }
    
    c. Unregister the subscription.
    IMSThing *thing = [[IMSThingManager sharedManager] buildThing:self.iotId];
    [thing unregisterThingObserver:self];

Native development for the scheduled task feature (Android)

  1. Create a proprietary app and download the SDK. For more information, see Create a proprietary app.

  2. Retrieve the device TSL model and timer properties.

    1. Create a com.aliyun.alink.linksdk.tmp.device.panel.PanelDevice object.

      PanelDevice panelDevice = new PanelDevice(iotId);
      panelDevice.init(null, new IPanelCallback() {
        @Override
        public void onComplete(boolean succeed, Object json) {
            // Implement the code based on your business logic.
        }
      })
    2. Retrieve the TSL model.

      Local scheduled tasks are a property of the TSL model. For more information about the TSL model, see TSL model SDK.

      panelDevice.getTslByCache(new IPanelCallback() {
          @Override
          public void onComplete(boolean succeed, Object json) {
        // Implement the code based on your business logic.
          }
      });
    3. Retrieve device properties.

      panelDevice.getPropertiesByCache(new IPanelCallback() {
        @Override
        public void onComplete(boolean succeed, @Nullable Object json) {
           // Implement the code based on your business logic.
          }
      }, null);

      The following is an example of the JSON format in the returned data.

      {
          "items":{
          "LocalTimer":  // The following is an example of timer data.
      [{
                  "LightSwitch": 1,
                  "ColorTemperature": 2000,
                  "Timer": "5 4 1,2,3",
                  "TimezoneOffset": 43200,
                  "Brightness": 0,
                  "Enable": 1,
                  "Targets": "LightSwitch",
                  "WorkMode": 0,
                  "IsValid": 1
      }]
                  }
      }
  3. Parse the timer data.

    The following method parses timer data based on the TSL model. For more information about the data structure, see TSL model 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. Set a timer.

    • Single device

      Use the PanelDevice#setProperties() method to update device properties.

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

      The following is an example of the request parameter in JSON format.

      {
          "iotId":"",
          "items":{
              "LocalTimer":[  // The following is an example of timer data.
                  {
                      "LightSwitch":1,
                      "ColorTemperature":2000,
                      "Timer":"5 4 1,2,3",
                      "TimezoneOffset":43200,
                      "Brightness":0,
                      "Enable":1,
                      "Targets":"LightSwitch",
                      "WorkMode":0,
                      "IsValid":1
                  }
              ]
          }
      }
    • Group control device

      Use the PanelGroup#setGroupProperties() method to update device properties.

      The initialization method for PanelGroup is similar to that of panelDevice. The difference is that you must pass the groupId for group control.

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

      The following is an example of the request parameter in JSON format.

      {
          "controlGroupId":"",
          "items":{
              "LocalTimer":[   // The following is an example of timer data.
                  {
                      "LightSwitch":1,
                      "ColorTemperature":2000,
                      "Timer":"5 4 1,2,3",
                      "TimezoneOffset":43200,
                      "Brightness":0,
                      "Enable":1,
                      "Targets":"LightSwitch",
                      "WorkMode":0,
                      "IsValid":1
                  }
              ]
          }
      }
  5. Subscribe to local timer property values.

    Real-time updates are often used to handle cases where device properties are set from multiple clients at the same time.

    // API: /app/down/thing/properties. The device reports 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) {
                        }
                    });

    The following is an example of the JSON format in the returned data.

    {
        "items":{
        "LocalTimer":  // The following is an example of timer data.
    [{
                "LightSwitch": 1,
                "ColorTemperature": 2000,
                "Timer": "5 4 1,2,3",
                "TimezoneOffset": 43200,
                "Brightness": 0,
                "Enable": 1,
                "Targets": "LightSwitch",
                "WorkMode": 0,
                "IsValid": 1
    }]
                }
    }

General instructions

  • Cron expression (the Timer field in the LocalTimer structure)

    The full format for a CronTrigger configuration in the timer property is: [Minute] [Hour] [Day of month] [Month] [Day of week]

    • * represents all possible values. For example, an asterisk in the minute field means the task triggers every minute. In the hour, day-of-month, or month fields, it means every hour, every day, or every month, respectively.

    • - represents a range of values. For example, 10-12 in the hour field means the task triggers at 10:00, 11:00, and 12:00.

    • , separates multiple values. For example, 2,3,4,5,6 in the day-of-week field means the task triggers on weekdays from Monday to Friday.

    • / indicates an incremental trigger. For example, 5/15 in the minute field indicates that the trigger starts at the 5th minute and runs every 15 minutes.

    • L stands for "last". In the day-of-month field, it means the last day of the month. In the day-of-week field, it can mean the last day of the week (Saturday or 7). When used with a number in the day-of-week field, it means the last instance of that day in the month. For example, 6L means the last Friday of the month, where 6 represents Friday.

    • W represents the nearest weekday to a given day. For example, 15W triggers on the nearest weekday to the 15th of the month. # specifies the nth weekday of the month. For example, 6#3 means the third Friday of the month.

  • Timezone (the Timezoneoffset field in the LocalTimer structure)

    The built-in Calendar class in Android has issues handling Daylight Saving Time (DST). In regions that observe winter time and daylight saving time, use the Instant class from Java 8 or another method to calculate the time offset. The following is sample code.

     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;
            }
        }
  • The Targets field

    When you add multiple actions to a LocalTimer, the modified fields must also be added to the Targets field. If you do not add the fields, all actions must be set again for the local timer to be saved correctly.

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