Add a rules engine so I can be notified when I forget to close my garage door.

This commit is contained in:
MarkBryanMilligan
2021-07-15 23:34:15 -05:00
parent de50645a2c
commit 3d5cd6500f
81 changed files with 2044 additions and 231 deletions

View File

@@ -0,0 +1,147 @@
package com.lanternsoftware.rules;
import com.lanternsoftware.dataaccess.currentmonitor.CurrentMonitorDao;
import com.lanternsoftware.dataaccess.currentmonitor.MongoCurrentMonitorDao;
import com.lanternsoftware.dataaccess.rules.MongoRulesDataAccess;
import com.lanternsoftware.dataaccess.rules.RulesDataAccess;
import com.lanternsoftware.datamodel.currentmonitor.Account;
import com.lanternsoftware.datamodel.rules.Action;
import com.lanternsoftware.datamodel.rules.ActionType;
import com.lanternsoftware.datamodel.rules.Criteria;
import com.lanternsoftware.datamodel.rules.Event;
import com.lanternsoftware.datamodel.rules.EventId;
import com.lanternsoftware.datamodel.rules.EventType;
import com.lanternsoftware.datamodel.rules.Rule;
import com.lanternsoftware.rules.actions.ActionImpl;
import com.lanternsoftware.util.CollectionUtils;
import com.lanternsoftware.util.DateUtils;
import com.lanternsoftware.util.LanternFiles;
import com.lanternsoftware.util.dao.DaoSerializer;
import com.lanternsoftware.util.dao.mongo.MongoConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.TimeZone;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class RulesEngine {
protected static final Logger LOG = LoggerFactory.getLogger(RulesEngine.class);
private static RulesEngine INSTANCE;
private final ExecutorService executor = Executors.newCachedThreadPool();
private final RulesDataAccess dao;
private final CurrentMonitorDao cmDao;
private final Map<ActionType, ActionImpl> actions = new HashMap<>();
private final Map<Integer, EventTimeTask> timeTasks = new HashMap<>();
private Timer timer;
public static RulesEngine instance() {
if (INSTANCE == null)
INSTANCE = new RulesEngine();
return INSTANCE;
}
public RulesEngine() {
ServiceLoader.load(ActionImpl.class).forEach(_action->actions.put(_action.getType(), _action));
dao = new MongoRulesDataAccess(MongoConfig.fromDisk(LanternFiles.OPS_PATH + "mongo.cfg"));
cmDao = new MongoCurrentMonitorDao(MongoConfig.fromDisk(LanternFiles.OPS_PATH + "mongo.cfg"));
timer = new Timer("RulesEngine Timer");
}
public void start() {
for (String id : cmDao.getProxy().queryForField(Account.class, null, "_id")) {
scheduleNextTimeEventForAccount(DaoSerializer.toInteger(id));
}
}
public RulesDataAccess dao() {
return dao;
}
public void fireEvent(Event _event) {
if (_event.getType() != EventType.TIME)
dao.putEvent(_event);
executor.submit(()->{
TimeZone tz = TimeZone.getTimeZone("America/Chicago"); //TODO: Get from the current monitor account
List<Rule> rules = CollectionUtils.filter(dao.getRulesForAccount(_event.getAccountId()), _r->_r.triggers(_event));
if (!rules.isEmpty()) {
for (Rule rule : rules) {
List<Event> events = CollectionUtils.asArrayList(_event);
List<Criteria> critNeedingData = rule.getCriteriaNeedingData(_event);
if (!critNeedingData.isEmpty()) {
Set<EventId> eventsToGet = CollectionUtils.transformToSet(critNeedingData, Criteria::toEventId);
for (EventId id : eventsToGet) {
Event event = dao.getMostRecentEvent(_event.getAccountId(), id.getType(), id.getSourceId());
if (event != null)
events.add(event);
}
}
if (rule.isMet(events, tz)) {
for (Action action : CollectionUtils.makeNotNull(rule.getActions())) {
ActionImpl impl = actions.get(action.getType());
impl.invoke(rule, events, action);
}
}
}
}
});
}
private void scheduleNextTimeEventForAccount(int _accountId) {
TimeZone tz = TimeZone.getTimeZone("America/Chicago"); //TODO: Get from the current monitor account
EventTimeTask nextTask = timeTasks.remove(_accountId);
if (nextTask != null)
nextTask.cancel();
List<Rule> rules = CollectionUtils.filter(dao.getRulesForAccount(_accountId), _r->CollectionUtils.anyQualify(_r.getAllCriteria(), _c->_c.getType() == EventType.TIME));
if (rules.isEmpty())
return;
Collection<Date> dates = CollectionUtils.aggregate(rules, _r->CollectionUtils.transform(_r.getAllCriteria(), _c->_c.getNextTriggerDate(tz)));
Date nextDate = CollectionUtils.getSmallest(dates);
LOG.info("Scheduling next time event for account {} at {}", _accountId, DateUtils.format("MM/dd/yyyy HH:mm:ss", nextDate));
nextTask = new EventTimeTask(_accountId, nextDate);
timer.schedule(nextTask, nextDate);
}
public static void shutdown() {
if (INSTANCE == null)
return;
INSTANCE.executor.shutdown();
INSTANCE.dao.shutdown();
INSTANCE.cmDao.shutdown();
INSTANCE.timer.cancel();
INSTANCE.timer = null;
INSTANCE = null;
}
private class EventTimeTask extends TimerTask {
private final int accountId;
private final Date eventTime;
EventTimeTask(int _accountId, Date _eventTime) {
accountId = _accountId;
eventTime = _eventTime;
}
@Override
public void run() {
LOG.info("Firing time event for account {}", accountId);
Event event = new Event();
event.setAccountId(accountId);
event.setTime(eventTime);
event.setType(EventType.TIME);
fireEvent(event);
scheduleNextTimeEventForAccount(accountId);
}
}
}

View File

@@ -0,0 +1,52 @@
package com.lanternsoftware.rules.actions;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.AndroidConfig;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.Message;
import com.lanternsoftware.datamodel.rules.Alert;
import com.lanternsoftware.datamodel.rules.FcmDevice;
import com.lanternsoftware.datamodel.rules.Rule;
import com.lanternsoftware.rules.RulesEngine;
import com.lanternsoftware.util.LanternFiles;
import com.lanternsoftware.util.dao.DaoSerializer;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileInputStream;
import java.util.List;
public abstract class AbstractAlertAction implements ActionImpl {
protected static final Logger logger = LoggerFactory.getLogger(AbstractAlertAction.class);
protected static final FirebaseMessaging messaging;
static {
FirebaseMessaging m = null;
try {
FileInputStream is = new FileInputStream(LanternFiles.OPS_PATH + "google_account_key.json");
FirebaseOptions options = FirebaseOptions.builder().setCredentials(GoogleCredentials.fromStream(is)).build();
m = FirebaseMessaging.getInstance(FirebaseApp.initializeApp(options));
IOUtils.closeQuietly(is);
}
catch (Exception _e) {
logger.error("Failed to load google credentials", _e);
}
messaging = m;
}
protected void sendAlert(Rule _rule, Alert _alert) {
List<FcmDevice> devices = RulesEngine.instance().dao().getFcmDevicesForAccount(_rule.getAccountId());
if (devices.isEmpty())
return;
for (FcmDevice device : devices) {
Message msg = Message.builder().setToken(device.getToken()).putData("payload", DaoSerializer.toBase64ZipBson(_alert)).putData("payloadClass", Alert.class.getCanonicalName()).setAndroidConfig(AndroidConfig.builder().setPriority(AndroidConfig.Priority.HIGH).setDirectBootOk(true).build()).build();
try {
messaging.send(msg);
} catch (Exception _e) {
logger.error("Failed to send message to account {}, device {}", _rule.getAccountId(), device.getName(), _e);
}
}
}
}

View File

@@ -0,0 +1,13 @@
package com.lanternsoftware.rules.actions;
import com.lanternsoftware.datamodel.rules.Action;
import com.lanternsoftware.datamodel.rules.ActionType;
import com.lanternsoftware.datamodel.rules.Event;
import com.lanternsoftware.datamodel.rules.Rule;
import java.util.List;
public interface ActionImpl {
ActionType getType();
void invoke(Rule _rule, List<Event> _event, Action _action);
}

View File

@@ -0,0 +1,22 @@
package com.lanternsoftware.rules.actions;
import com.lanternsoftware.datamodel.rules.Action;
import com.lanternsoftware.datamodel.rules.ActionType;
import com.lanternsoftware.datamodel.rules.Alert;
import com.lanternsoftware.datamodel.rules.Event;
import com.lanternsoftware.datamodel.rules.Rule;
import com.lanternsoftware.util.CollectionUtils;
import java.util.List;
public class MobileAlertAction extends AbstractAlertAction {
@Override
public ActionType getType() {
return ActionType.MOBILE_ALERT_EVENT_DESCRIPTION;
}
@Override
public void invoke(Rule _rule, List<Event> _event, Action _action) {
sendAlert(_rule, new Alert(CollectionUtils.transformToCommaSeparated(_event, Event::getEventDescription)));
}
}

View File

@@ -0,0 +1,22 @@
package com.lanternsoftware.rules.actions;
import com.lanternsoftware.datamodel.rules.Action;
import com.lanternsoftware.datamodel.rules.ActionType;
import com.lanternsoftware.datamodel.rules.Alert;
import com.lanternsoftware.datamodel.rules.Event;
import com.lanternsoftware.datamodel.rules.Rule;
import com.lanternsoftware.util.CollectionUtils;
import java.util.List;
public class MobileAlertStatic extends AbstractAlertAction {
@Override
public ActionType getType() {
return ActionType.MOBILE_ALERT_STATIC;
}
@Override
public void invoke(Rule _rule, List<Event> _event, Action _action) {
sendAlert(_rule, new Alert(_action.getDescription()));
}
}

View File

@@ -0,0 +1,24 @@
package com.lanternsoftware.rules.servlet;
import com.lanternsoftware.datamodel.rules.Event;
import com.lanternsoftware.rules.RulesEngine;
import com.lanternsoftware.util.dao.auth.AuthCode;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/event")
public class EventServlet extends SecureServlet {
@Override
protected void post(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) {
Event event = getRequestPayload(_req, Event.class);
if (event == null) {
_rep.setStatus(400);
return;
}
event.setAccountId(_authCode.getAccountId());
RulesEngine.instance().fireEvent(event);
}
}

View File

@@ -0,0 +1,24 @@
package com.lanternsoftware.rules.servlet;
import com.lanternsoftware.datamodel.rules.FcmDevice;
import com.lanternsoftware.rules.RulesEngine;
import com.lanternsoftware.util.dao.auth.AuthCode;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/fcm")
public class FcmServlet extends SecureServlet {
@Override
protected void post(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) {
FcmDevice device = getRequestPayload(_req, FcmDevice.class);
if (device == null) {
_rep.setStatus(400);
return;
}
device.setAccountId(_authCode.getAccountId());
RulesEngine.instance().dao().putFcmDevice(device);
}
}

View File

@@ -0,0 +1,39 @@
package com.lanternsoftware.rules.servlet;
import com.lanternsoftware.util.cryptography.AESTool;
import com.lanternsoftware.util.dao.DaoSerializer;
import com.lanternsoftware.util.dao.auth.AuthCode;
import com.lanternsoftware.util.servlet.LanternServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public abstract class SecureServlet extends LanternServlet {
private static final AESTool aes = AESTool.authTool();
@Override
protected void doGet(HttpServletRequest _req, HttpServletResponse _rep) {
AuthCode authCode = DaoSerializer.fromZipBson(aes.decryptFromBase64(_req.getHeader("auth_code")), AuthCode.class);
if (authCode == null) {
_rep.setStatus(401);
return;
}
get(authCode, _req, _rep);
}
protected void get(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) {
}
@Override
protected void doPost(HttpServletRequest _req, HttpServletResponse _rep) {
AuthCode authCode = DaoSerializer.fromZipBson(aes.decryptFromBase64(_req.getHeader("auth_code")), AuthCode.class);
if (authCode == null) {
_rep.setStatus(401);
return;
}
post(authCode, _req, _rep);
}
protected void post(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) {
}
}

View File

@@ -0,0 +1,2 @@
com.lanternsoftware.rules.actions.MobileAlertAction
com.lanternsoftware.rules.actions.MobileAlertStatic