From 2479587d8f3fc63c2048aa0815b767aa71b33eec Mon Sep 17 00:00:00 2001 From: Manuel Krombholz <s3866577@msx.tu-dresden.de> Date: Fri, 28 May 2021 07:58:16 +0000 Subject: [PATCH] Resolve "Add processing frequency option for items" --- eraser-base/src/main/jastadd/Item.jrag | 18 +++++++- eraser-base/src/main/jastadd/LastChanged.jrag | 20 +++++++++ eraser-base/src/main/jastadd/Navigation.jrag | 43 +++++++++++++++++++ eraser-base/src/main/jastadd/Resolving.jrag | 13 ++++++ eraser-base/src/main/jastadd/Rules.relast | 4 +- eraser-base/src/main/jastadd/eraser.flex | 3 ++ eraser-base/src/main/jastadd/eraser.parser | 26 ++++++++--- eraser-base/src/main/jastadd/main.relast | 2 +- eraser-base/src/main/jastadd/shem.relast | 12 ++++-- .../st/eraser/parser/EraserParserHelper.java | 24 +++++++++-- .../de/tudresden/inf/st/eraser/RulesTest.java | 30 +++++++++++++ 11 files changed, 178 insertions(+), 17 deletions(-) create mode 100644 eraser-base/src/main/jastadd/LastChanged.jrag diff --git a/eraser-base/src/main/jastadd/Item.jrag b/eraser-base/src/main/jastadd/Item.jrag index ece1fd87..94ebdce1 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 00000000..e79bfe7c --- /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 dd4e283a..984e1029 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 e1d793c1..2d3a99f7 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 22176613..e6c3ab01 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 21987f14..621d9d04 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 1a055ec6..f58525a3 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 b9fd3e6c..e12d23dc 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 a71a0ede..62cb2550 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 01b91770..32be358b 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 633627c6..f4da998f 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); -- GitLab