Merge branch '28-implement-websockets-for-sensors-and-motion-sensors' into 'dev'

Resolve "Implement fake updates trough websockets for sensors and motion sensors"

Closes #28

See merge request sa4-2020/the-sanmarinoes/backend!37
This commit is contained in:
Claudio Maggioni 2020-03-15 14:56:15 +01:00
commit 7e53ccd608
15 changed files with 230 additions and 16 deletions

View file

@ -1,3 +1,5 @@
<!-- vim: set ts=2 sw=2 et tw=80: -->
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@ -25,6 +27,8 @@ connection.onmessage = function(evt) {
"<img src='https://maggioni.xyz/astley.gif'>"; "<img src='https://maggioni.xyz/astley.gif'>";
} else if (data.authenticated === false) { } else if (data.authenticated === false) {
malusa.innerHTML = "<h1>Authentication error</h1>"; malusa.innerHTML = "<h1>Authentication error</h1>";
} else {
malusa.innerHTML += "<p><pre>" + JSON.stringify(JSON.parse(evt.data), null, 2) + "</pre></p>";
} }
}; };

View file

@ -3,8 +3,10 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
@EnableScheduling
@EnableJpaRepositories("ch.usi.inf.sa4.sanmarinoes.smarthut.models") @EnableJpaRepositories("ch.usi.inf.sa4.sanmarinoes.smarthut.models")
public class SmarthutApplication { public class SmarthutApplication {
public static void main(String[] args) { public static void main(String[] args) {

View file

@ -20,7 +20,7 @@ public class GsonConfig {
return converter; return converter;
} }
private Gson gson() { public static Gson gson() {
final GsonBuilder builder = new GsonBuilder(); final GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter()); builder.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter());
builder.addSerializationExclusionStrategy(new AnnotationExclusionStrategy()); builder.addSerializationExclusionStrategy(new AnnotationExclusionStrategy());

View file

@ -6,6 +6,8 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveReguest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensor; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensor;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensorRepository; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensorRepository;
import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint;
import java.security.Principal;
import java.util.List; import java.util.List;
import javax.validation.Valid; import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -19,6 +21,8 @@ public class MotionSensorController {
@Autowired private MotionSensorRepository motionSensorService; @Autowired private MotionSensorRepository motionSensorService;
@Autowired private SensorSocketEndpoint sensorSocketEndpoint;
@GetMapping @GetMapping
public List<MotionSensor> findAll() { public List<MotionSensor> findAll() {
return toList(motionSensorService.findAll()); return toList(motionSensorService.findAll());
@ -38,6 +42,36 @@ public class MotionSensorController {
return motionSensorService.save(newMS); return motionSensorService.save(newMS);
} }
/**
* Updates detection status of given motion sensor and propagates update throgh socket
*
* @param sensor the motion sensor to update
* @param detected the new detection status
* @return the updated motion sensor
*/
public MotionSensor updateDetectionFromMotionSensor(MotionSensor sensor, boolean detected) {
sensor.setDetected(detected);
final MotionSensor toReturn = motionSensorService.save(sensor);
sensorSocketEndpoint.broadcast(sensor, motionSensorService.findUser(sensor.getId()));
return toReturn;
}
@PutMapping("/{id}/detect")
public MotionSensor updateDetection(
@PathVariable("id") Long sensorId,
@RequestParam("detected") boolean detected,
final Principal principal)
throws NotFoundException {
return updateDetectionFromMotionSensor(
motionSensorService
.findByIdAndUsername(sensorId, principal.getName())
.orElseThrow(NotFoundException::new),
detected);
}
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void delete(@PathVariable("id") long id) { public void delete(@PathVariable("id") long id) {
motionSensorService.deleteById(id); motionSensorService.deleteById(id);

View file

@ -5,6 +5,9 @@ import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SensorSaveRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SensorSaveRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint;
import java.math.BigDecimal;
import java.security.Principal;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import javax.validation.Valid; import javax.validation.Valid;
@ -19,6 +22,8 @@ public class SensorController {
@Autowired private SensorRepository sensorRepository; @Autowired private SensorRepository sensorRepository;
@Autowired private SensorSocketEndpoint sensorSocketEndpoint;
@GetMapping @GetMapping
public List<Sensor> findAll() { public List<Sensor> findAll() {
return toList(sensorRepository.findAll()); return toList(sensorRepository.findAll());
@ -35,10 +40,40 @@ public class SensorController {
newSensor.setSensor(s.getSensor()); newSensor.setSensor(s.getSensor());
newSensor.setName(s.getName()); newSensor.setName(s.getName());
newSensor.setRoomId(s.getRoomId()); newSensor.setRoomId(s.getRoomId());
newSensor.setValue(s.getValue());
return sensorRepository.save(newSensor); return sensorRepository.save(newSensor);
} }
/**
* Updates the sensor with new measurement and propagates update through websocket
*
* @param sensor the sensor to update
* @param value the new measurement
* @return the updated sensor
*/
public Sensor updateValueFromSensor(Sensor sensor, BigDecimal value) {
sensor.setValue(value);
final Sensor toReturn = sensorRepository.save(sensor);
sensorSocketEndpoint.broadcast(sensor, sensorRepository.findUser(sensor.getId()));
return toReturn;
}
@PutMapping("/{id}/value")
public Sensor updateValue(
@PathVariable("id") Long sensorId,
@RequestParam("value") BigDecimal value,
final Principal principal)
throws NotFoundException {
return updateValueFromSensor(
sensorRepository
.findByIdAndUsername(sensorId, principal.getName())
.orElseThrow(NotFoundException::new),
value);
}
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") long id) { public void deleteById(@PathVariable("id") long id) {
sensorRepository.deleteById(id); sensorRepository.deleteById(id);

View file

@ -2,6 +2,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import java.math.BigDecimal;
import javax.persistence.EnumType; import javax.persistence.EnumType;
import javax.persistence.Enumerated; import javax.persistence.Enumerated;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@ -28,6 +29,8 @@ public class SensorSaveRequest {
@Enumerated(value = EnumType.STRING) @Enumerated(value = EnumType.STRING)
private Sensor.SensorType sensor; private Sensor.SensorType sensor;
@NotNull private BigDecimal value;
/** /**
* The room this device belongs in, as a foreign key id. To use when updating and inserting from * The room this device belongs in, as a foreign key id. To use when updating and inserting from
* a REST call. * a REST call.
@ -60,4 +63,12 @@ public class SensorSaveRequest {
public void setSensor(Sensor.SensorType sensor) { public void setSensor(Sensor.SensorType sensor) {
this.sensor = sensor; this.sensor = sensor;
} }
public BigDecimal getValue() {
return value;
}
public void setValue(BigDecimal value) {
this.value = value;
}
} }

View file

@ -1,5 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import javax.persistence.*; import javax.persistence.*;
@ -26,6 +27,11 @@ public abstract class Device {
@ApiModelProperty(hidden = true) @ApiModelProperty(hidden = true)
private long id; private long id;
@ManyToOne
@JoinColumn(name = "room_id", updatable = false, insertable = false)
@GsonExclude
private Room room;
/** /**
* The room this device belongs in, as a foreign key id. To use when updating and inserting from * The room this device belongs in, as a foreign key id. To use when updating and inserting from
* a REST call. * a REST call.

View file

@ -1,6 +1,8 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
@ -10,4 +12,23 @@ import org.springframework.data.repository.query.Param;
*/ */
public interface DeviceRepository<T extends Device> extends CrudRepository<T, Long> { public interface DeviceRepository<T extends Device> extends CrudRepository<T, Long> {
List<T> findByRoomId(@Param("roomId") long roomId); List<T> findByRoomId(@Param("roomId") long roomId);
/**
* Finds devices by their id and a username
*
* @param id the device id
* @param username a User's username
* @return an optional device, empty if none found
*/
@Query("SELECT d FROM Device d JOIN d.room r JOIN r.user u WHERE d.id = ?1 AND u.username = ?2")
Optional<T> findByIdAndUsername(Long id, String username);
/**
* Find the user associated with a device through a room
*
* @param deviceId the device id
* @return a user object
*/
@Query("SELECT u FROM Device d JOIN d.room r JOIN r.user u WHERE d.id = ?1")
User findUser(Long deviceId);
} }

View file

@ -1,5 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import javax.persistence.*; import javax.persistence.*;
@ -128,6 +129,11 @@ public class Room {
@Column(name = "image", columnDefinition = "TEXT") @Column(name = "image", columnDefinition = "TEXT")
private String image; private String image;
@ManyToOne
@JoinColumn(name = "user_id", updatable = false, insertable = false)
@GsonExclude
private User user;
/** /**
* User that owns the house this room is in as a foreign key id. To use when updating and * User that owns the house this room is in as a foreign key id. To use when updating and
* inserting from a REST call. * inserting from a REST call.

View file

@ -1,6 +1,8 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import java.math.BigDecimal;
import java.util.Map;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.EnumType; import javax.persistence.EnumType;
@ -11,6 +13,12 @@ import javax.validation.constraints.NotNull;
@Entity @Entity
public class Sensor extends InputDevice { public class Sensor extends InputDevice {
public static final Map<SensorType, BigDecimal> TYPICAL_VALUES =
Map.of(
SensorType.TEMPERATURE, new BigDecimal(17.0),
SensorType.HUMIDITY, new BigDecimal(40.0),
SensorType.LIGHT, new BigDecimal(1000));
/** Type of sensor, i.e. of the thing the sensor measures. */ /** Type of sensor, i.e. of the thing the sensor measures. */
public enum SensorType { public enum SensorType {
/** A sensor that measures temperature in degrees celsius */ /** A sensor that measures temperature in degrees celsius */
@ -27,8 +35,8 @@ public class Sensor extends InputDevice {
} }
/** The value of this sensor according to its sensor type */ /** The value of this sensor according to its sensor type */
@Column(nullable = false) @Column(nullable = false, length = 10, precision = 1)
private int value; private BigDecimal value;
/** The type of this sensor */ /** The type of this sensor */
@Column(nullable = false) @Column(nullable = false)
@ -42,19 +50,22 @@ public class Sensor extends InputDevice {
public void setSensor(SensorType sensor) { public void setSensor(SensorType sensor) {
this.sensor = sensor; this.sensor = sensor;
// TODO: setup hook for sockets live update
} }
public int getValue() { public BigDecimal getValue() {
return this.value; return this.value;
} }
public void setValue(int newValue) { public void setValue(BigDecimal newValue) {
this.value = newValue; this.value = newValue;
} }
public Sensor() { public Sensor() {
super("sensor"); super("sensor");
} }
@Override
public String toString() {
return "Sensor{" + "value=" + value + ", sensor=" + sensor + '}';
}
} }

View file

@ -1,6 +1,7 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import java.util.Objects;
import javax.persistence.*; import javax.persistence.*;
/** A user of the Smarthut application */ /** A user of the Smarthut application */
@ -105,4 +106,22 @@ public class User {
+ isEnabled + isEnabled
+ '}'; + '}';
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return id.equals(user.id)
&& name.equals(user.name)
&& username.equals(user.username)
&& password.equals(user.password)
&& email.equals(user.email)
&& isEnabled.equals(user.isEnabled);
}
@Override
public int hashCode() {
return Objects.hash(id, name, username, password, email, isEnabled);
}
} }

View file

@ -0,0 +1,62 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.scheduled;
import ch.usi.inf.sa4.sanmarinoes.smarthut.controller.MotionSensorController;
import ch.usi.inf.sa4.sanmarinoes.smarthut.controller.SensorController;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensorRepository;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SensorRepository;
import java.math.BigDecimal;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.StreamSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/** Generates fake sensor (and motion sensor) updates as required by milestone one */
@Component
public class SensorUpdateTasks {
@Autowired private SensorRepository sensorRepository;
@Autowired private MotionSensorRepository motionSensorRepository;
@Autowired private SensorController sensorController;
@Autowired private MotionSensorController motionSensorController;
/** Generates fake sensor updates every two seconds with a +/- 1.25% error */
@Scheduled(fixedRate = 2000)
public void sensorFakeUpdate() {
StreamSupport.stream(sensorRepository.findAll().spliterator(), true)
.forEach(
sensor ->
sensorController.updateValueFromSensor(
sensor,
Sensor.TYPICAL_VALUES
.get(sensor.getSensor())
.multiply(
new BigDecimal(
0.9875 + Math.random() / 40))));
}
/**
* Generate fake motion detections in all motion detectors every 20 seconds for 2 seconds at
* most
*/
@Scheduled(fixedDelay = 20000)
public void motionSensorFakeUpdate() {
StreamSupport.stream(motionSensorRepository.findAll().spliterator(), true)
.forEach(
sensor -> {
motionSensorController.updateDetectionFromMotionSensor(sensor, true);
CompletableFuture.delayedExecutor(
(long) (Math.random() * 2000), TimeUnit.MILLISECONDS)
.execute(
() ->
motionSensorController
.updateDetectionFromMotionSensor(
sensor, false));
});
}
}

View file

@ -1,5 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; package ch.usi.inf.sa4.sanmarinoes.smarthut.socket;
import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonConfig;
import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils; import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
@ -18,7 +19,7 @@ import org.springframework.stereotype.Component;
@Component @Component
public class AuthenticationMessageListener { public class AuthenticationMessageListener {
private Gson gson = new Gson(); private Gson gson = GsonConfig.gson();
private JWTTokenUtils jwtTokenUtils; private JWTTokenUtils jwtTokenUtils;
@ -43,8 +44,6 @@ public class AuthenticationMessageListener {
return new MessageHandler.Whole<>() { return new MessageHandler.Whole<>() {
@Override @Override
public void onMessage(final String message) { public void onMessage(final String message) {
System.out.println(message);
if (message == null) { if (message == null) {
acknowledge(false); acknowledge(false);
return; return;

View file

@ -2,6 +2,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.socket;
import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.didThrow; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.didThrow;
import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonConfig;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
import com.google.common.collect.HashMultimap; import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
@ -16,7 +17,7 @@ import org.springframework.stereotype.Component;
@Component @Component
public class SensorSocketEndpoint extends Endpoint { public class SensorSocketEndpoint extends Endpoint {
private Gson gson = new Gson(); private Gson gson = GsonConfig.gson();
private AuthenticationMessageListener authenticationMessageListener; private AuthenticationMessageListener authenticationMessageListener;
@ -60,7 +61,7 @@ public class SensorSocketEndpoint extends Endpoint {
final Collection<Session> sessions = authorizedClients.get(u); final Collection<Session> sessions = authorizedClients.get(u);
return sessions.stream() return sessions.stream()
.parallel() .parallel()
.filter(didThrow(s -> s.getAsyncRemote().sendObject(gson.toJson(message)))) .filter(didThrow(s -> s.getBasicRemote().sendText(gson.toJson(message))))
.count(); .count();
} }

View file

@ -1,8 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.utils; package ch.usi.inf.sa4.sanmarinoes.smarthut.utils;
import java.util.List; import java.util.List;
import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
@ -11,14 +9,19 @@ import java.util.stream.StreamSupport;
public final class Utils { public final class Utils {
private Utils() {} private Utils() {}
@FunctionalInterface
public interface ConsumerWithException<T> {
void apply(T input) throws Throwable;
}
public static <T> List<T> toList(Iterable<T> iterable) { public static <T> List<T> toList(Iterable<T> iterable) {
return StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList()); return StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList());
} }
public static <T> Predicate<T> didThrow(Function<T, Future<?>> consumer) { public static <T> Predicate<T> didThrow(ConsumerWithException<T> consumer) {
return (t) -> { return (t) -> {
try { try {
consumer.apply(t).get(); consumer.apply(t);
return true; return true;
} catch (Throwable e) { } catch (Throwable e) {
System.err.println(e.getMessage()); System.err.println(e.getMessage());