diff --git a/eraser-base/src/main/jastadd/Item.jrag b/eraser-base/src/main/jastadd/Item.jrag index ece1fd875c26b2d6f4bc94aa712f9bfe56a8884b..94ebdce1bc189ccbfa73b767ccacd9488936fc5c 100644 --- a/eraser-base/src/main/jastadd/Item.jrag +++ b/eraser-base/src/main/jastadd/Item.jrag @@ -21,6 +21,8 @@ aspect ItemHandling { eq ItemWithDoubleState.getStateAsString() = Double.toString(getState()); eq ItemWithStringState.getStateAsString() = getState(); + syn LastChanged Item.getLastChanged() = new LastChanged(); + //--- getStateAsDouble --- syn double Item.getStateAsDouble(); // TupleHSB and String work like default @@ -285,7 +287,7 @@ aspect ItemHandling { //--- stateUpdated --- /** - * Called, whenever the state of an item is updated. Does various things including: + * Called, whenever the state of an item is updated. Does various things (except it has no parent) including: * <ul> * <li>Send the new state via MQTT</li> * <li>Send the new state to Influx DB</li> @@ -294,6 +296,7 @@ aspect ItemHandling { * @param shouldSendState whether to send the new state (currently affects MQTT and Influx) */ protected void Item.stateUpdated(boolean shouldSendState) { + if (getParent() == null) { return; } if (shouldSendState) { try { // sendState() refined in MQTT and Influx aspect @@ -302,9 +305,13 @@ aspect ItemHandling { logger.catching(e); } } - getItemObserver().apply(); + if (this.getLastChanged().checkStateProcessingTime(relevantFrequencySetting())) { + this.getLastChanged().afterStateChangeProcessed(); + getItemObserver().apply(); + } } + //--- sendState --- protected void Item.sendState() throws Exception { for (MachineLearningModel model : getRelevantInMachineLearningModels()) { @@ -459,6 +466,11 @@ aspect ItemHandling { } + //--- onReceiveStateChange --- + protected void Item.onReceiveStateChange() { + //Instant lastChange = this. + } + @@ -506,4 +518,6 @@ aspect ItemHandling { syn String ItemUpdate.getNewStateAsString(); eq ItemUpdateColor.getNewStateAsString() = getNewHSB().toString(); eq ItemUpdateDouble.getNewStateAsString() = Double.toString(getNewValue()); + + } diff --git a/eraser-base/src/main/jastadd/LastChanged.jrag b/eraser-base/src/main/jastadd/LastChanged.jrag new file mode 100644 index 0000000000000000000000000000000000000000..e79bfe7c8c394154f78307ca629ff64ddfd4eb2f --- /dev/null +++ b/eraser-base/src/main/jastadd/LastChanged.jrag @@ -0,0 +1,20 @@ +aspect LastChanged { + public void LastChanged.afterStateChangeProcessed() { + this.setValue(Instant.now()); + } + + public boolean LastChanged.checkStateProcessingTime(FrequencySetting FrequencySetting) { + if (FrequencySetting==null) { + return true; + } + double frequency = FrequencySetting.getEventProcessingFrequency(); + Instant lastStateChange = this.getValue(); + if (lastStateChange==null) { + return true; + } + + return lastStateChange.toEpochMilli() + (1/frequency)*1000 < Instant.now().toEpochMilli(); + + } + +} \ No newline at end of file diff --git a/eraser-base/src/main/jastadd/Navigation.jrag b/eraser-base/src/main/jastadd/Navigation.jrag index dd4e283a155b68dd432303debd93dd094aee06eb..984e1029e1aa7817396d6676f7772d9bc824d1ba 100644 --- a/eraser-base/src/main/jastadd/Navigation.jrag +++ b/eraser-base/src/main/jastadd/Navigation.jrag @@ -11,6 +11,48 @@ aspect Navigation { return result; } + //--- enclosingGroup --- + inh Group Group.enclosingGroup(); + inh Group Item.enclosingGroup(); + eq Group.getItem().enclosingGroup() = this; + eq Group.getGroup().enclosingGroup() = this; + eq SmartHomeEntityModel.getGroup().enclosingGroup() = null; + eq SmartHomeEntityModel.getActivityItem().enclosingGroup() = null; + + //--- relevantFrequencySetting --- + syn FrequencySetting Group.relevantFrequencySetting() { + // first, use value defined on group itself, if any + if (this.hasFrequencySetting()) { + return this.getFrequencySetting(); + } + + // recursively use enclosing group and use value from there, if any + Group parent = enclosingGroup(); + if (parent != null) { + return parent.relevantFrequencySetting(); + } + + // if top-level group without FrequencySetting + return null; + + } + syn FrequencySetting Item.relevantFrequencySetting() { + // first, use value defined on item itself, if any + if (this.hasFrequencySetting()) { + return this.getFrequencySetting(); + } + + // use enclosing group and use value from there, if any + Group parent = enclosingGroup(); + if (parent != null) { + return parent.relevantFrequencySetting(); + } + + // if top-level item without FrequencySetting + return null; + } + + //--- addItems --- private void SmartHomeEntityModel.addItems(java.util.List<Item> result, JastAddList<Group> groups) { groups.forEach(group -> group.getItemList().forEach(item -> result.add(item))); } @@ -56,4 +98,5 @@ aspect Navigation { eq Root.getRule().getRoot() = this; eq Root.getUser().getRoot() = this; eq Root.getLocation().getRoot() = this; + eq Root.getFrequencySetting().getRoot() = this; } diff --git a/eraser-base/src/main/jastadd/Resolving.jrag b/eraser-base/src/main/jastadd/Resolving.jrag index e1d793c1587afcd16b232197a2e87b21c9d6f08b..2d3a99f76bb63228bf41c315c9c9a86a768653cd 100644 --- a/eraser-base/src/main/jastadd/Resolving.jrag +++ b/eraser-base/src/main/jastadd/Resolving.jrag @@ -98,9 +98,22 @@ aspect Resolving { return java.util.Optional.empty(); } + syn java.util.Optional<FrequencySetting> Root.resolveFrequencySetting(String performanceId) { + for (FrequencySetting performance : getFrequencySettingList()) { + if (performance.getLabel().equals(performanceId)) { + return java.util.Optional.of(performance); + } + } + return java.util.Optional.empty(); + } + // implementing resolving for relations refine RefResolverStubs eq StateSyncGroup.resolveTargetItemByToken(String id, int position) { return getRoot().getSmartHomeEntityModel().resolveItem(id).orElseThrow(() -> new RuntimeException("Item '" + id + "' not found!")); } + refine RefResolverStubs eq ASTNode.globallyResolveFrequencySettingByToken(String id) { + return getRoot().resolveFrequencySetting(id).orElseThrow(() -> new RuntimeException("FrequencySetting '" + id + "' not found!")); + } + } diff --git a/eraser-base/src/main/jastadd/Rules.relast b/eraser-base/src/main/jastadd/Rules.relast index 22176613130d4fd2802379fffb6d406819a2cd04..e6c3ab0138742149407d3a6279a958c2cc551ee4 100644 --- a/eraser-base/src/main/jastadd/Rules.relast +++ b/eraser-base/src/main/jastadd/Rules.relast @@ -2,7 +2,8 @@ Rule ::= Condition* Action* ; abstract Condition ; ItemStateCheckCondition : Condition ::= ItemStateCheck ; -ItemStateChangeCondition : Condition ::= Item; +ItemStateChangeCondition : Condition ; +rel ItemStateChangeCondition.Item -> Item; ExpressionCondition : Condition ::= LogicalExpression ; abstract Action ; NoopAction : Action ; @@ -29,5 +30,6 @@ MultiplyDoubleToStateAction : SetStateAction ::= <Multiplier:double> ; ItemObserver ::= ; rel ItemObserver.TriggeredRule* <-> Rule.Observer* ; +FrequencySetting : LabelledModelElement ::= <EventProcessingFrequency:double> ; StateSyncGroup : Rule ; rel StateSyncGroup.TargetItem* -> Item; diff --git a/eraser-base/src/main/jastadd/eraser.flex b/eraser-base/src/main/jastadd/eraser.flex index 21987f14a9762c3f7c9de8dadd405ad5431ad7b5..621d9d044c0dc2d82695221d28c1da16f6dadd5c 100644 --- a/eraser-base/src/main/jastadd/eraser.flex +++ b/eraser-base/src/main/jastadd/eraser.flex @@ -87,6 +87,9 @@ Comment = "//" [^\n\r]+ "incoming" { return sym(Terminals.INCOMING); } "items" { return sym(Terminals.ITEMS); } "itemType" { return sym(Terminals.ITEM_TYPE); } +"FrequencySetting" { return sym(Terminals.FREQUENCY_SETTING); } +"performance" { return sym(Terminals.PERFORMANCE); } +"procFreq" { return sym(Terminals.PROCESS_FREQUENCY); } "label" { return sym(Terminals.LABEL); } "links" { return sym(Terminals.LINKS); } "metaData" { return sym(Terminals.META_DATA); } diff --git a/eraser-base/src/main/jastadd/eraser.parser b/eraser-base/src/main/jastadd/eraser.parser index 1a055ec64b0166fa0bea7882e6e0921e7d280976..f58525a329bc0e858283e1b5f95ef14297a3f14c 100644 --- a/eraser-base/src/main/jastadd/eraser.parser +++ b/eraser-base/src/main/jastadd/eraser.parser @@ -41,6 +41,7 @@ Root goal = | influx_root.ir goal.r {: r.setInfluxRoot(ir); return r; :} | machine_learning_root.ml goal.r {: r.setMachineLearningRoot(ml); return r; :} | rule.rule goal.r {: r.addRule(rule); return r; :} + | frequency_setting.ip goal.r {: r.addFrequencySetting(ip); return r; :} | thing.t {: return eph.createRoot(t); :} | item.i {: return eph.createRoot(); :} | group.g {: return eph.createRoot(g); :} @@ -53,6 +54,7 @@ Root goal = | influx_root.ir {: return eph.createRoot(ir); :} | machine_learning_root.ml {: return eph.createRoot(ml); :} | rule.rule {: return eph.createRoot(rule); :} + | frequency_setting.ip {: return eph.createRoot(ip); :} ; %left RB_ROUND; @@ -128,28 +130,28 @@ Item item = | ITEM COLON item_body.ib SEMICOLON {: return eph.retype(new DefaultItem(), ib); :} ; -// ITEM_TYPE Item: id="" label="" state="" category="" topic="" metaData={"key":"value"} ; +// ITEM_TYPE Item: id="" label="" state="" category="" topic="" performance="" metaData={"key":"value"} ; Item item_body = ID EQUALS TEXT.n item_body.i {: return eph.setID(i, n); :} | LABEL EQUALS TEXT.n item_body.i {: i.setLabel(n); return i; :} | STATE EQUALS TEXT.n item_body.i {: i.setStateFromString(n); return i; :} | TOPIC EQUALS TEXT.n item_body.i {: return eph.setTopic(i, n); :} | CATEGORY EQUALS TEXT.n item_body.i {: return eph.setCategory(i, n); :} + | PERFORMANCE EQUALS frequency_setting_ref.ip item_body.i {: i.setFrequencySetting(ip); return i; :} | META_DATA EQUALS string_map.md item_body.i {: return eph.setMetaData(i, md); :} | {: return eph.createItem(); :} ; -Item item_ref = - TEXT.n {: return Item.createRef(n); :} - ; +Item item_ref = TEXT.n {: return Item.createRef(n); :}; +FrequencySetting frequency_setting_ref = TEXT.n {: return FrequencySetting.createRef(n); :}; Group group = GROUP COLON group_body.gb SEMICOLON {: return gb; :} ; -// Group: id="" groups=["GROUP_ID", "GROUP_ID"] items=["ITEM_ID", "ITEM_ID"] aggregation=""; -// Group: id="" groups=["GROUP_ID", "GROUP_ID"] items=["ITEM_ID", "ITEM_ID"] aggregation="" ("",""); +// Group: id="" groups=["GROUP_ID", "GROUP_ID"] items=["ITEM_ID", "ITEM_ID"] performance="" aggregation=""; +// Group: id="" groups=["GROUP_ID", "GROUP_ID"] items=["ITEM_ID", "ITEM_ID"] performance="" aggregation="" ("",""); Group group_body = ID EQUALS TEXT.n group_body.g {: return eph.setID(g, n); :} | LABEL EQUALS TEXT.n group_body.g {: g.setLabel(n); return g; :} @@ -158,6 +160,7 @@ Group group_body = | AGGREGATION EQUALS TEXT.n group_body.g {: return eph.setSimpleAggregationFunction(g, n); :} | AGGREGATION EQUALS TEXT.n round_string_list.params group_body.g {: return eph.setParameterizedAggregationFunction(g, n, params); :} + | PERFORMANCE EQUALS frequency_setting_ref.ip group_body.g {: g.setFrequencySetting(ip); return g; :} | {: return new Group(); :} ; @@ -365,3 +368,14 @@ IntegerKeyMap integer_map_body = return result; :} ; + +FrequencySetting frequency_setting = + FREQUENCY_SETTING COLON frequency_setting_body.ipb SEMICOLON {: return ipb; :} + ; + +// FrequencySetting: id="" procFreq="" persFreq=""; +FrequencySetting frequency_setting_body = + ID EQUALS TEXT.n frequency_setting_body.ip {: return eph.setID(ip, n); :} + | PROCESS_FREQUENCY EQUALS TEXT.n frequency_setting_body.ip {: ip.setEventProcessingFrequency(Double.parseDouble(n)); return ip; :} + | {: return new FrequencySetting(); :} + ; diff --git a/eraser-base/src/main/jastadd/main.relast b/eraser-base/src/main/jastadd/main.relast index b9fd3e6ca271b94dd0a59c1f596c3e4cad241d58..e12d23dc7185d244d079e3aa5d712a4290c8a2f4 100644 --- a/eraser-base/src/main/jastadd/main.relast +++ b/eraser-base/src/main/jastadd/main.relast @@ -1,5 +1,5 @@ // ---------------- Main ------------------------------ -Root ::= SmartHomeEntityModel User* MqttRoot InfluxRoot MachineLearningRoot Rule* Location* ; +Root ::= SmartHomeEntityModel User* MqttRoot InfluxRoot MachineLearningRoot Rule* Location* FrequencySetting*; // ---------------- Users ------------------------------ User : LabelledModelElement ; diff --git a/eraser-base/src/main/jastadd/shem.relast b/eraser-base/src/main/jastadd/shem.relast index a71a0ede3307b937a77798d9713ddbae197d323e..62cb25506245a5f7d04dcea483cfa11c07d1d179 100644 --- a/eraser-base/src/main/jastadd/shem.relast +++ b/eraser-base/src/main/jastadd/shem.relast @@ -1,6 +1,7 @@ // ---------------- openHAB ------------------------------ SmartHomeEntityModel ::= Thing* Group* ThingType* ChannelType* ChannelCategory* ItemCategory* /ActivityItem:Item/ ; + abstract ModelElement ::= <ID:String> ; abstract LabelledModelElement : ModelElement ::= <Label:String> ; abstract DescribableModelElement : LabelledModelElement ::= <Description:String> ; @@ -25,10 +26,9 @@ rel Channel.LinkedItem* <-> Item.Channel? ; Parameter : DescribableModelElement ::= <Type:ParameterValueType> [DefaultValue:ParameterDefaultValue] <Context:String> <Required:boolean> ; ParameterDefaultValue ::= <Value:String> ; -abstract Item : LabelledModelElement ::= <_fetched_data:boolean> MetaData:ItemMetaData* /ItemObserver/; - - +abstract Item : LabelledModelElement ::= <_fetched_data:boolean> MetaData:ItemMetaData* /ItemObserver/ /LastChanged/; rel Item.Category? -> ItemCategory ; +rel Item.FrequencySetting? -> FrequencySetting ; abstract ItemWithBooleanState : Item ::= <_state:boolean> ; abstract ItemWithStringState : Item ::= <_state:String> ; @@ -51,8 +51,14 @@ ItemMetaData ::= <Key:String> <Value:String> ; ItemCategory ::= <Name:String> ; +LastChanged ::= <Value:Instant> ; + + Group : LabelledModelElement ::= Group* Item* [AggregationFunction:GroupAggregationFunction] ; +rel Group.FrequencySetting? -> FrequencySetting ; + abstract GroupAggregationFunction ; SimpleGroupAggregationFunction : GroupAggregationFunction ::= <FunctionName:SimpleGroupAggregationFunctionName> ; ParameterizedGroupAggregationFunction : GroupAggregationFunction ::= <FunctionName:ParameterizedGroupAggregationFunctionName> <Param1:String> <Param2:String> ; + diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/parser/EraserParserHelper.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/parser/EraserParserHelper.java index 01b91770dc1d4a58dfd4f03033ea1068c9282a98..32be358baf48a7cd2af133d0fe3e1a3dca97aa02 100644 --- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/parser/EraserParserHelper.java +++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/parser/EraserParserHelper.java @@ -24,16 +24,18 @@ public class EraserParserHelper { private Map<String, Parameter> parameterMap = new HashMap<>(); private Map<String, Item> itemMap = new HashMap<>(); private Map<String, Group> groupMap = new HashMap<>(); + private Map<String, FrequencySetting> FrequencySettingMap = new HashMap<>(); private Map<Thing, String> missingThingTypeMap = new HashMap<>(); private Map<Channel, String> missingChannelTypeMap = new HashMap<>(); private Map<Item, String> missingTopicMap = new HashMap<>(); private Map<Item, String> missingItemCategoryMap = new HashMap<>(); + private Map<Designator, String> missingItemForDesignator = new HashMap<>(); private Map<Thing, Iterable<String>> missingChannelListMap = new HashMap<>(); private Map<Channel, Iterable<String>> missingItemLinkListMap = new HashMap<>(); - private Map<Item, Iterable<String>> missingControllingListMap = new HashMap<>(); + private Map<Group, Iterable<String>> missingSubGroupListMap = new HashMap<>(); private Map<Group, Iterable<String>> missingItemListMap = new HashMap<>(); private Map<ThingType, Iterable<String>> missingChannelTypeListMap = new HashMap<>(); @@ -94,19 +96,20 @@ public class EraserParserHelper { resolveList(itemMap, missingItemLinkListMap, Channel::addLinkedItem); resolveList(groupMap, missingSubGroupListMap, Group::addGroup); resolveList(itemMap, missingItemListMap, this::addItemToGroup); - resolveList(channelTypeMap, missingChannelTypeListMap, ThingType::addChannelType); resolveList(parameterMap, missingParameterListMap, ThingType::addParameter); - resolveList(itemMap, missingControllingListMap, Item::addControlling); + createUnknownGroupIfNecessary(); createChannelCategories(); createItemCategories(); + if (checkUnusedElements) { checkUnusedElements(); } this.root.treeResolveAll(); + this.root.doFullTraversal(); } private void addItemToGroup(Group group, Item item) { @@ -226,6 +229,12 @@ public class EraserParserHelper { return thing; } + public FrequencySetting setID(FrequencySetting FrequencySetting, String id) { + FrequencySetting.setID(id); + FrequencySettingMap.put(id,FrequencySetting); + return FrequencySetting; + } + public ThingType setID(ThingType thingType, String id) { thingType.setID(id); thingTypeMap.put(id, thingType); @@ -259,6 +268,7 @@ public class EraserParserHelper { return c; } + public Channel setLinks(Channel c, StringList linkNames) { missingItemLinkListMap.put(c, linkNames); return c; @@ -300,6 +310,7 @@ public class EraserParserHelper { itemWithCorrectType.setID(prototype.getID()); itemWithCorrectType.setLabel(prototype.getLabel()); itemWithCorrectType.setMetaDataList(prototype.getMetaDataList()); + itemWithCorrectType.setFrequencySetting(prototype.getFrequencySetting()); if (!(itemWithCorrectType instanceof ActivityItem)) { String state = prototype.getStateAsString(); itemWithCorrectType.disableSendState(); @@ -312,7 +323,6 @@ public class EraserParserHelper { } moveMissingForRetype(itemWithCorrectType, prototype, missingTopicMap); - moveMissingForRetype(itemWithCorrectType, prototype, missingControllingListMap); moveMissingForRetype(itemWithCorrectType, prototype, missingItemCategoryMap); itemMap.put(prototype.getID(), itemWithCorrectType); @@ -470,6 +480,12 @@ public class EraserParserHelper { return result; } + public Root createRoot(FrequencySetting FrequencySetting) { + Root result = createRoot(); + result.addFrequencySetting(FrequencySetting); + return result; + } + //+++ newStuff (to be categorized) +++ public Designator createDesignator(String itemName) { Designator result = new Designator(); diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/RulesTest.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/RulesTest.java index 633627c6bf85e4ddd363865b01bb62155919ae1e..f4da998f546f33e1ad976b2d9aef08b329185966 100644 --- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/RulesTest.java +++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/RulesTest.java @@ -250,6 +250,7 @@ public class RulesTest { assertEquals(2, counter.get(item), "Change of item to 7 should not trigger the rule, check2 violated"); } + @Test public void testStateSyncGroupRewriteStructure() { // init StateSyncGroup @@ -915,6 +916,35 @@ public class RulesTest { assertEquals(2, counter.get(null), "Rule was not executed two times"); } + @Test + public void testFrequencySetting() { + + TestUtils.ModelAndItem mai = createModelAndItem(0); + NumberItem numberItem = mai.item; + + FrequencySetting itemPerformance = new FrequencySetting(); + itemPerformance.setEventProcessingFrequency(10); + numberItem.setFrequencySetting(itemPerformance); + + Rule rule = new Rule(); + CountingAction counter = new CountingAction(); + rule.addAction(counter); + rule.activateFor(numberItem); + numberItem.setState(1); + numberItem.setState(2); + assertEquals(1, counter.get(numberItem), "Action was triggered although FrequencySetting too small"); + counter.reset(); + waitMillis(100); + numberItem.setState(3); + assertEquals(1, counter.get(numberItem), "Action wasn't triggered although frequency FrequencySetting is small enough"); + counter.reset(); + numberItem.setState(4); + numberItem.setState(5); + assertEquals(0, counter.get(numberItem), "Action was triggered although FrequencySetting too small"); + counter.reset(); + + } + private static void waitMillis(int millis) { try { Thread.sleep(millis);