开发App端的本地定时功能
本地定时相对云端定时而言,是指当设备离线时,也能自动执行定时任务。本文档介绍自有App中的定时功能的开发实践。
前提条件
已完成开发设备端本地定时功能中的配置控制台参数和设备端开发。
概述
生活物联网平台提供App 基于Native开发定时功能(静态方式) 的开发方式。
Native开发定时功能(静态方式)
静态方式开发即开发native的本地定时功能。该方式开发的App定时功能用户体验更好,但对技术要求较高,且无法动态更新,需要通过发布App版本来更新。
Native开发定时功能(iOS)
创建一个自有App,并完成SDK下载。详细操作请参见创建自有App。
获取本地定时的属性字段。
本地定时是属于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; } }]; } }
获取设备端的定时器列表数据。
// 本地定时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]; } }
设置(编辑/创建/删除等)一个定时器。
@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) { }
订阅本地定时属性值。
以监听定时的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)
创建一个自有App,并完成SDK下载。详细操作请参见创建自有App。
获取设备TSL模型和定时属性。
创建
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) { // 根据自己的业务逻辑实现相关代码 } })
获取TSL模型。
本地定时是属于TSL的一个属性,更多TSL模型的介绍请参见物模型SDK。
panelDevice.getTslByCache(new IPanelCallback() { @Override public void onComplete(boolean succeed, Object json) { // 根据自己的业务逻辑实现相关代码 } });
获取设备属性。
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 }] } }
解析定时数据。
根据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); } } }
设置一个定时器。
单设备
使用
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 } ] } }
订阅本地定时属性值。
实时更新常用来处理多端同时设置设备属性的情况。
// 接口:/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 }